Bladeren bron

Migrate to Nuxt.js

Andrea Franceschini 2 jaren geleden
bovenliggende
commit
0051d84bf4
100 gewijzigde bestanden met toevoegingen van 2488 en 3792 verwijderingen
  1. 8 4
      .gitignore
  2. 0 18
      .htaccess
  3. 3 0
      README.md
  4. 5 0
      app.vue
  5. 64 0
      assets/css/tailwind.css
  6. 10 0
      components/Breakpoint.vue
  7. 69 0
      components/CookieMonster.vue
  8. 131 0
      components/Cubetti.vue
  9. 23 0
      components/Footer.vue
  10. 41 0
      components/MainMenu.vue
  11. 19 0
      components/ScrollDownSVG.vue
  12. 115 0
      components/TopBanner.vue
  13. 0 26
      config.rb
  14. 0 0
      css/print.css
  15. 0 0
      css/screen.css
  16. 59 0
      error.vue
  17. BIN
      files/cv-aforg.pdf
  18. BIN
      images/banner-half.jpg
  19. BIN
      images/banner-original.jpg
  20. BIN
      images/hscroller-01.png
  21. 0 77
      index.php
  22. 0 368
      js/H5F.js
  23. 0 19
      js/contact.js
  24. 0 322
      js/html5.min.js
  25. 0 1
      js/jquery.min.js
  26. 0 147
      js/js.cookie.js
  27. 0 3
      js/picturefill.min.js
  28. 0 77
      js/scripts.js
  29. 29 0
      layouts/default.vue
  30. 0 46
      libs/utils.php
  31. 10 0
      middleware/matomoRoutes.global.ts
  32. 99 0
      nuxt.config.ts
  33. 0 77
      oldcss/cookie.css
  34. 0 43
      oldcss/sass/_base.scss
  35. 0 102
      oldcss/sass/_contact.scss
  36. 0 25
      oldcss/sass/_cv.scss
  37. 0 17
      oldcss/sass/_development.scss
  38. 0 50
      oldcss/sass/cookie.scss
  39. 0 437
      oldcss/sass/screen.scss
  40. 0 825
      oldcss/screen.css
  41. 32 22
      package.json
  42. 21 0
      pages/404.vue
  43. 0 59
      pages/_footer.php
  44. 0 11
      pages/_ga.php
  45. 0 82
      pages/_head.php
  46. 0 25
      pages/_header.php
  47. 0 12
      pages/_main_menu.php
  48. 0 28
      pages/api/mailer.php
  49. 0 22
      pages/cbprivacy.php
  50. 29 0
      pages/cbprivacy.vue
  51. 0 42
      pages/colophon.php
  52. 55 0
      pages/colophon.vue
  53. 0 69
      pages/contact.php
  54. 72 0
      pages/contact.vue
  55. 54 0
      pages/cookies.vue
  56. 0 228
      pages/cv.php
  57. 269 0
      pages/cv.vue
  58. 0 238
      pages/development.php
  59. 261 0
      pages/development.vue
  60. 0 17
      pages/errors/403.php
  61. 0 20
      pages/errors/404.php
  62. 0 37
      pages/eumyths.php
  63. 43 0
      pages/eumyths.vue
  64. 0 45
      pages/home.php
  65. 139 0
      pages/index.vue
  66. 27 21
      pages/qualia.vue
  67. 0 50
      pages/research.php
  68. 72 0
      pages/research.vue
  69. 0 57
      pages/writing.php
  70. 77 0
      pages/writing.vue
  71. 21 0
      plugins/fontawesome.js
  72. 27 0
      plugins/matomo.client.ts
  73. 593 0
      plugins/modernizr.client.js
  74. 5 0
      plugins/persistedstate.ts
  75. 0 22
      postcss.config.js
  76. 5 0
      public/.htaccess
  77. 0 0
      public/ads.txt
  78. 0 0
      public/files/FHCI/Marasoiu-supervision-1.pdf
  79. 0 0
      public/files/FHCI/Marasoiu-supervision-2.pdf
  80. 0 0
      public/files/FHCI/index.html
  81. 0 0
      public/files/LC64_inkscape.pdf
  82. 0 0
      public/files/LC67_inkscape.pdf
  83. 0 0
      public/files/LC73_drupal.pdf
  84. 0 0
      public/files/LC74_drupal.pdf
  85. 0 0
      public/files/LC75_drupal.pdf
  86. 0 0
      public/files/ProgC/supervision_1.html
  87. 1 1
      public/files/ProgC/supervision_2.html
  88. 0 0
      public/files/ProgC/supervision_3.html
  89. 0 0
      public/files/ccers20-inequality-poster-combo.pdf
  90. 0 0
      public/files/crusade_bsc_privacy_policy.html
  91. 0 0
      public/files/csmc2017.pdf
  92. 0 0
      public/files/dmrn8-poster-combo.pdf
  93. 0 0
      public/files/franceschini-andrea-cv.pdf
  94. 0 0
      public/files/icmcsmc2014-poster.pdf
  95. 0 0
      public/files/icmcsmc2014.pdf
  96. 0 0
      public/files/jmte-13-1.pdf
  97. 0 0
      public/files/las2019.pdf
  98. 0 0
      public/files/master_thesis.pdf
  99. 0 0
      public/files/met2016.pdf
  100. 0 0
      public/files/msca-2021-seal-of-excellence.pdf

+ 8 - 4
.gitignore

@@ -1,5 +1,9 @@
-.DS_Store
-.sass-cache
-npm-debug.log
 node_modules
-pcss
+*.log*
+.nuxt
+.nitro
+.cache
+.output
+.env
+dist
+*.DS_Store

+ 0 - 18
.htaccess

@@ -1,18 +0,0 @@
-RewriteEngine on
-RewriteRule "(^|/)\.(?!well-known\/)" 404 [L]
-RewriteCond %{REQUEST_FILENAME} !-f
-RewriteCond %{REQUEST_FILENAME} !-d
-RewriteCond %{REQUEST_URI} !=/favicon.ico
-RewriteRule ^(.*)$ index.php [L,QSA]
-
-RewriteRule "^.htaccess" 404 [L]
-RewriteRule "^config.rb" 404 [L]
-RewriteRule "^.git(/|ignore)?" 404 [L]
-RewriteRule "^package(-lock)?.json" 404 [L]
-RewriteRule "^postcss.config.js" 404 [L]
-RewriteRule "^tailwind.config.js" 404 [L]
-RewriteRule "^oldcss" 404 [L]
-RewriteRule "^node_modules" 404 [L]
-RewriteRule "^libs" 404 [L]
-RewriteRule "^pcss" 404 [L]
-RewriteRule "^.DS_Store" 404 [L]

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+# Andrea Franceschini's personal (serious) website
+
+You'll find it [here](https://andreafranceschini.org).

+ 5 - 0
app.vue

@@ -0,0 +1,5 @@
+<template>
+  <NuxtLayout>
+    <NuxtPage />
+  </NuxtLayout>
+</template>

+ 64 - 0
assets/css/tailwind.css

@@ -0,0 +1,64 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+div.noscript {
+    background: white;
+    color: black;
+    font-size: 1.5rem;
+    padding: 1rem;
+
+    a {
+        text-decoration: underline;
+        color: dodgerblue;
+    }
+}
+
+h1, h2, h3, h4, h5, h6 {
+    font-family: "Oswald";
+}
+
+.prose p {
+    &.calltoaction {
+        text-align: center;
+        display: block;
+        margin: theme(spacing.6) 0;
+        padding: theme(spacing.6) 0;
+        background-color: #e7f2fd;
+        font-size: 1.5em;
+        font-family: Oswald,sans-serif;
+    }
+}
+.prose strong a {
+    font-weight: bold;
+}
+
+.prose figure {
+    position: relative;
+
+    img {
+        width: 100%;
+    }
+
+    figcaption {
+        position: absolute;
+        width: 100%;
+        bottom: 0;
+        font-size: .75rem;
+        color: #fff;
+        line-height: 1.75rem;
+        background: rgba(0,0,0,.5);
+        padding: 0 theme("spacing.3") .2em theme("spacing.3");
+        border-bottom-right-radius: .25rem;
+        border-bottom-left-radius: .25rem;
+
+        a {
+            color: white;
+            text-decoration: underline;
+        }
+
+        &:before {
+            content: "\203A\2002";
+        }
+    }
+}

+ 10 - 0
components/Breakpoint.vue

@@ -0,0 +1,10 @@
+<template>
+    <div class="flex items-center m-2 fixed bottom-0 right-0 border border-gray-400 rounded p-2 bg-gray-300 text-pink-600 text-sm">
+        Current breakpoint
+        <span class="ml-1 sm:hidden md:hidden lg:hidden xl:hidden">default (&lt; 640px)</span>
+        <span class="ml-1 hidden sm:inline md:hidden font-extrabold">sm</span>
+        <span class="ml-1 hidden md:inline lg:hidden font-extrabold">md</span>
+        <span class="ml-1 hidden lg:inline xl:hidden font-extrabold">lg</span>
+        <span class="ml-1 hidden xl:inline font-extrabold">xl</span>
+    </div>
+</template>

+ 69 - 0
components/CookieMonster.vue

@@ -0,0 +1,69 @@
+<template>
+    <div id="cookie_monster" :class="`print:hidden z-50 ${isOpen ? 'mix-blend-normal' : 'mix-blend-difference'}`">
+        <div v-if="isOpen" class="fixed bottom-2 right-2 left-2 sm:w-10/12 max-w-screen-md sm:mx-auto">
+            <div class="border-2 bg-white border-sky-700 px-4 pt-3 pb-4 overflow-scroll">
+                <client-only><fa-icon :icon="['fas', 'circle-xmark']" class="text-error text-4xl float-right" @click="isOpen = false" /></client-only>
+                <div class="pb-4">
+                    <p>I use <strong>cookies</strong> for <a href="/cookies">reasons</a>. Please read the reasons before setting your preferences.</p>
+                    <div class="sm:pl-4 pt-2 grid sm:grid-cols-12 grid-cols-6">
+                        <p><input type="checkbox" class="form-control toggle toggle-primary" v-model="consentStore.analytics" @change="(e: Event) => consentStore.analytics = (e.target as HTMLInputElement).checked" /></p>
+                        <p class="col-span-5 sm:col-span-11"><strong>Analytics</strong>: optional, but it'd be great if you agreed 😊</p>
+                    </div>
+                    <div class="sm:pl-4 pt-2 grid sm:grid-cols-12 grid-cols-6">
+                        <p><input type="checkbox" class="form-control toggle toggle-primary" v-model="consentStore.ads" @change="(e: Event) => consentStore.ads = (e.target as HTMLInputElement).checked" /></p>
+                        <p class="col-span-5 sm:col-span-11"><strong>Advertising</strong>: fully optional, feel free to skip this one.</p>
+                    </div>
+                    <p class="pt-2">You can read more on how I use cookies on <a href="/cookies">this page</a>.</p>
+                </div>
+                <div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
+                    <button class="btn btn-error" @click="rejectAll">Reject all</button>
+                    <button class="btn btn-info btn-outline" @click="acceptAll">Accept all</button>
+                    <button class="btn btn-success" @click="acceptMyChoice">Accept my choice</button>
+                </div>
+            </div>
+        </div>
+        <div v-else @click="isOpen = true" class="fixed max-w-0 bottom-2 sm:right-16 right-11 cursor-pointer">
+            <ClientOnly><fa-icon icon="fa-solid fa-cookie-bite" class="sm:text-6xl text-4xl text-sky-500" /></ClientOnly>
+        </div>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { useConsent } from '~~/store/consent';
+const consentStore = useConsent();
+
+const error = useError();
+const isOpen = useState('isOpen', () => !consentStore.preferenceGiven && error.value === undefined);
+
+const rejectAll = () => {
+    consentStore.analytics = false;
+    consentStore.ads = false;
+    consentStore.preferenceGiven = true;
+    isOpen.value = false;
+}
+
+const acceptAll = () => {
+    consentStore.analytics = true;
+    consentStore.ads = true;
+    consentStore.preferenceGiven = true;
+    isOpen.value = false;
+}
+
+const acceptMyChoice = () => {
+    consentStore.preferenceGiven = true;
+    isOpen.value = false;
+}
+</script>
+
+<style lang="pcss">
+#cookie_monster {
+    div {
+        width: 100%;
+    }
+
+    a {
+        color: theme(colors.sky.700);
+        text-decoration: underline;
+    }
+}
+</style>

+ 131 - 0
components/Cubetti.vue

@@ -0,0 +1,131 @@
+<template>
+    <div id="cubetti" ref="cubetti" />
+</template>
+
+<style lang="pcss" scoped>
+@media screen(sm) {
+    #cubetti {
+        position: absolute;
+        width: 200px !important;
+        height: 200px !important;
+        top: -55px !important;
+        left: 0px !important;
+    }
+}
+
+#cubetti {
+    position: absolute;
+    width: 120px;
+    height: 120px;
+    overflow: visible;
+    top: -24px;
+    left: -22px;
+}
+
+</style>
+
+<script lang="ts" setup>
+    import { AmbientLight } from 'three/src/lights/AmbientLight';
+    import { DirectionalLight } from 'three/src/lights/DirectionalLight';
+    import { BoxGeometry } from 'three/src/geometries/BoxGeometry';
+    import { Mesh } from 'three/src/objects/Mesh';
+    import { MeshStandardMaterial } from 'three/src/materials/MeshStandardMaterial';
+    import { PerspectiveCamera } from 'three/src/cameras/PerspectiveCamera';
+    import { Scene } from 'three/src/scenes/Scene';
+    import { Vector3 } from 'three/src/math/Vector3';
+    import { WebGLRenderer } from 'three/src/renderers/WebGLRenderer';
+    import anime from 'animejs';
+    import { randInt } from 'three/src/math/MathUtils';
+
+    const scene = new Scene()
+    const camera = new PerspectiveCamera(80, 1, 0.1, 1000)
+    const renderer = new WebGLRenderer({ antialias: true, alpha: true })
+    renderer.setPixelRatio(window.devicePixelRatio);
+    const directionalLight = new DirectionalLight(0x808080)
+    const ambientLight = new AmbientLight(0x808080*1.75)
+    const geometry = new BoxGeometry(1, 1, 1)
+    const blueMaterial = new MeshStandardMaterial({ color: 'dodgerblue' })
+    const greenMaterial = new MeshStandardMaterial({ color: 'greenyellow' })
+    const orangeMaterial = new MeshStandardMaterial({ color: 'orange' })
+    const blueCube = new Mesh(geometry, blueMaterial)
+    blueCube.position.set(-1, 0, 1)
+    const greenCube = new Mesh(geometry, greenMaterial)
+    greenCube.position.set(1, 0, 1)
+    const orangeCube = new Mesh(geometry, orangeMaterial)
+    orangeCube.position.set(0, 0, -1)
+    const cubeGroup = new Scene()
+    cubeGroup.add(blueCube, greenCube, orangeCube)
+
+    const cubettiAnims = [
+        { // group spin
+            targets: [cubeGroup.rotation],
+            keyframes: [
+                { y: 2 * Math.PI, duration: 1000 },
+            ],
+            easing: 'easeOutCubic',
+            duration: 1000,
+            complete: playRandomAnimation,
+        },
+        { // floating cubes
+            targets: [orangeCube.position, blueCube.position, greenCube.position],
+            keyframes: [
+                { y: -0.5, duration: 500/3 },
+                { y:  0.5, duration: 707/3 },
+                { y:  0.0, duration: 500/3 },
+            ],
+            easing: 'easeInOutSine',
+            delay: anime.stagger(500/3),
+            complete: playRandomAnimation,
+        },
+        { // rotating cubes
+            targets: [orangeCube.rotation, blueCube.rotation, greenCube.rotation],
+            x: randInt(0, 1) * 2 * Math.PI, y: randInt(0, 1) * 2 * Math.PI, z: randInt(0, 1) * 2 * Math.PI, duration: 1000,
+            easing: 'easeOutSine',
+            delay: anime.stagger(500/3),
+            complete: playRandomAnimation
+        }
+    ]
+    
+    const cubetti = ref()
+
+    function playRandomAnimation() {
+        setTimeout(() => {
+            anime(cubettiAnims[randInt(0, cubettiAnims.length-1)])
+        }, randInt(1000, 8000))
+    }
+
+    const animate = () => {
+        requestAnimationFrame(animate)
+        renderer.render(scene, camera)
+    }
+
+    const resizeCanvas = () => {
+        if (window.innerWidth < 640) {
+            renderer.setSize(120, 120);
+        } else {
+            renderer.setSize(200, 200);
+        }
+    }
+
+    onMounted(() => {
+        scene.add(camera)
+        scene.add(ambientLight, directionalLight)
+        scene.add(cubeGroup)
+        // renderer.setSize(200, 200)
+        resizeCanvas();
+        camera.position.copy((new Vector3(-2, 1.5, 2)).multiplyScalar(1.2))
+        camera.lookAt(0, 0, 0)
+        scene.background = null
+        cubetti.value.appendChild(renderer.domElement)
+
+        window.addEventListener('resize', resizeCanvas);
+
+        playRandomAnimation()
+
+        animate()
+    })
+
+    onUnmounted(() => {
+        window.removeEventListener('resize', resizeCanvas);
+    })
+</script>

+ 23 - 0
components/Footer.vue

@@ -0,0 +1,23 @@
+<template>
+    <div class="container mx-auto max-w-screen-lg sm:px-6 px-2">
+        <div class="text-white grid grid-cols-3 py-6">
+            <div class="col-start-1 col-span-3 sm:col-span-1">
+                <p><a href="/colophon"><client-only><fa-icon icon="fa-regular fa-circle-question" class="pr-2" /></client-only>About this site…</a></p>
+            </div>
+            <div class="col-start-1 col-span-2 sm:col-start-2 sm:col-span-2">
+                <p class="mt-2 sm:mt-0 sm:text-right"><client-only><fa-icon icon="fa-regular fa-copyright" class="pr-2" /></client-only>Unless otherwise specified, the content of this site is licensed as <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a>.</p>
+            </div>
+        </div>
+    </div>
+</template>
+
+<style lang="pcss" scoped>
+* {
+    color: theme(colors.slate.400);
+    font-family: "Open Sans";
+}
+
+a {
+    color: theme(colors.slate.100);
+}
+</style>

+ 41 - 0
components/MainMenu.vue

@@ -0,0 +1,41 @@
+<template>
+    <nav id="main_menu" class="order-last bg-black print:hidden">
+        <div class="inner_box overflow-x-scroll overflow-y-hidden whitespace-no-wrap border-b-[1px] border-b-slate-700">
+        <ul class="flex md:px-0 py-2 justify-start ti:justify-around mx-auto max-w-screen-md">
+            <li class="ml-0"><a href="/" class="text-white pr-3">Home</a></li>
+                <li><a href="/cv" class="text-white pr-3">CV</a></li>
+                <li><a href="/research" class="text-white pr-3">Research</a></li>
+                <li><a href="/development" class="text-white pr-3">Development</a></li>
+                <li><a href="/writing" class="text-white pr-3">Writing</a></li>
+                <li><a href="/contact" class="text-white pr-3">Contact</a></li>
+            </ul>
+        </div>
+    </nav>
+</template>
+
+<style lang="pcss" scoped>
+#main_menu {
+    background: black url('/images/hscroller-01.png') bottom center no-repeat;
+
+    @media screen(sm) {
+        background: black;
+    }
+
+    li {
+        font-family: Oswald;
+        font-size: theme(fontSize.base);
+        text-align: center;
+        width: 100%;
+        padding: theme(spacing.3) theme(spacing.3);
+
+        a {
+            color: white;
+        }
+
+        @media screen(sm) {
+            font-size: theme(fontSize.xl);
+            padding: theme(spacing.3) theme(spacing.0);
+        }
+    }
+}
+</style>

+ 19 - 0
components/ScrollDownSVG.vue

@@ -0,0 +1,19 @@
+<template>
+    <svg width="50" height="50" viewBox="0 0 13.229166 13.229167" version="1.1" id="svg5" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+        <defs id="defs2" />
+        <g
+            id="layer1">
+            <path
+                style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
+                d="M 1.5875,6.3499999 6.6145833,10.583333 11.641667,6.3499999"
+                id="path1867" />
+            <ellipse
+                style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
+                id="path3518"
+                cx="6.614583"
+                cy="4.6302066"
+                rx="1.9843727"
+                ry="1.9843731" />
+        </g>
+    </svg>
+</template>

+ 115 - 0
components/TopBanner.vue

@@ -0,0 +1,115 @@
+<template>
+    <div :class="`top_banner print:hidden page ${pageClasses.join(' ')}`">
+        <div class="container mx-auto max-w-screen-xl z-10">
+            <a href="/">
+                <ClientOnly>
+                    <span class="sr-only">Home page</span>
+                    <div class="cubetti_bg" />
+                    <Cubetti />
+                </ClientOnly>
+            </a>
+            <div v-if="!isIndexPage">
+                <div class="headline sm:ml-52 ml-24 pt-2 sm:pt-4"><h1 class="sm:text-4xl text-3xl text-white"><slot name="headline">Default Headline from TopBanner</slot></h1></div>
+                <div class="subheadline sm:ml-56 ml-28"><h2 class="sm:text-2xl text-xl text-white"><slot name="tagline">Default tagline from TopBanner</slot></h2></div>
+            </div>
+            <div v-if="isIndexPage" class="ml-2">
+                <div class="headline sm:-ml-48 -ml-36 pt-3 sm:pt-4 text-center"><h1 class="sm:text-3xl text-2xl text-white">Hello</h1></div>
+                <div class="subheadline ml-14 -mt-8 text-center"><h2 class="sm:text-5xl text-4xl text-white">I'm Andrea</h2></div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<style lang="pcss" scoped>
+html.no-webp {
+    body {
+        &.research .top_banner {
+            background-image: url("/images/banner_research-2x.jpg");
+        }
+        &.writing .top_banner {
+            background-image: url("/images/banner_writing-2x.jpg");
+        }
+        .top_banner {
+            background-image: url("/images/banner.jpg");
+        }
+    }
+}
+body {
+    &.research .top_banner {
+        background-image: url("/images/banner_research-2x.webp");
+    }
+    &.writing .top_banner {
+        background-image: url("/images/banner_writing-2x.webp");
+        background-position: 0 100%;
+    }
+}
+.top_banner {
+    background-image: url("/images/banner.webp");
+    background-position: center;
+    background-size: cover;
+    height: 80px;
+
+    @media screen(sm) {
+        height: 100px;
+    }
+
+    .container {
+        position: relative;
+    
+        .headline, .subheadline {
+            text-shadow: 0px 0px 4px theme(colors.sky.900);
+        }
+
+        .cubetti_bg {
+            position: absolute;
+            background: white;
+            height: calc(150px * 0.58);
+            width: calc(140px * 0.55);
+            top: 8px;
+            left: 8px;
+            border-radius: 6px;
+
+            &:before {
+                content: "";
+                background: linear-gradient(0deg, theme(colors.sky.900), transparent);
+                opacity: 0.25;
+                position: absolute;
+                height: 100%;
+                width: 100%;
+                z-index: -1;
+                filter: blur(10px);
+            }
+
+            @media screen(sm) {
+                position: absolute;
+                background: white;
+                height: 150px;
+                width: 140px;
+                top: -10px;
+                left: 45px;
+                border-radius: 12px;
+
+                &:before {
+                    content: "";
+                    background: linear-gradient(0deg, theme(colors.sky.900), transparent);
+                    opacity: 0.25;
+                    position: absolute;
+                    height: 100%;
+                    width: 100%;
+                    z-index: -1;
+                    filter: blur(10px);
+                }
+            }
+        }
+    }
+}
+
+</style>
+
+<script lang="ts" setup>
+const pageClasses = useState<string[]>('pageClasses').value || [];
+
+const route = useRoute();
+const error = useError();
+const isIndexPage = computed(() => route.name === 'index' && error.value === undefined);
+</script>

+ 0 - 26
config.rb

@@ -1,26 +0,0 @@
-require 'compass/import-once/activate'
-# Require any additional compass plugins here.
-require 'susy'
-require 'breakpoint'
-# Set this to the root of your project when deployed:
-http_path = "/"
-css_dir = "css"
-sass_dir = "css/sass"
-images_dir = "images"
-javascripts_dir = "js"
-
-# You can select your preferred output style here (can be overridden via the command line):
-# output_style = :expanded or :nested or :compact or :compressed
-output_style = :expanded
-# To enable relative paths to assets via compass helper functions. Uncomment:
-# relative_assets = true
-
-# To disable debugging comments that display the original location of your selectors. Uncomment:
-# line_comments = false
-
-
-# If you prefer the indented syntax, you might want to regenerate this
-# project again passing --syntax sass, or you can uncomment this:
-# preferred_syntax = :sass
-# and then run:
-# sass-convert -R --from scss --to sass css/sass scss && rm -rf sass && mv scss sass

File diff suppressed because it is too large
+ 0 - 0
css/print.css


File diff suppressed because it is too large
+ 0 - 0
css/screen.css


+ 59 - 0
error.vue

@@ -0,0 +1,59 @@
+<template>
+    <div>
+        <TopBanner>
+            <template #headline><slot name="headline">{{ error.statusCode }}</slot></template>
+            <template #tagline><slot name="tagline">means I f*cked up…</slot></template>
+        </TopBanner>
+        <div id="content_box" :class="`flex flex-col bg-white pt-12 sm:pt-24`">
+            <CookieMonster />
+            <MainMenu />
+
+            <div class="container mx-auto max-w-screen-lg sm:px-12 px-2 prose">
+                <article class="body grid grid-cols-1 gap-x-12 gap-y-0">
+                    <h3 class="text-5xl text-center -mt-6 sm:-mt-16">
+                        Sorry, I messed up.
+                    </h3>
+
+                    <p class="pt-12 py-6">Please copy the following error report and <a href="/contact" class="text-sky-500 underline">get in touch</a>.</p>
+
+                    <div>
+                        <span @click="copyToClipboard" class="btn btn-info btn-outline float-right">Copy to clipboard</span>
+                        <div id="errorReport">    
+                            <p>Error code: {{ error.statusCode }}</p>
+                            <p>Message: {{ error.message }}</p>
+                            <p>URL: {{ error.url }}</p>
+                            <p v-if="error.data">Additional data: {{ error.data }}</p>
+                            <div v-html="error.description" />
+                        </div>
+                    </div>
+
+                    <p>&nbsp;</p><p>&nbsp;</p>
+                </article>
+            </div>
+        </div>
+        <Footer />
+    </div>
+</template>
+
+<style lang="pcss">
+article.body h2 {
+    @apply mt-0;
+}
+</style>
+
+<script setup>
+const props = defineProps({
+    error: Object
+})
+
+const copyToClipboard = () => {
+    navigator.permissions.query({ name: 'clipboard-write' }).then(result => {
+        if (result.state === 'granted' || result.state === 'prompt') {
+            const element = document.getElementById('errorReport');
+            navigator.clipboard.writeText(element.innerText).then(() => {
+                alert("Error report copied. Head over to the contact section and get in touch!");
+            });
+        }
+    });
+}
+</script>

BIN
files/cv-aforg.pdf


BIN
images/banner-half.jpg


BIN
images/banner-original.jpg


BIN
images/hscroller-01.png


+ 0 - 77
index.php

@@ -1,77 +0,0 @@
-<?php
-function d() {
-	
-}
-
-function error($code = 403) {
-	if($code == 404) {
-		header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
-		header("Status: 404 Not Found");
-		$_SERVER['REDIRECT_STATUS'] = 404;
-		return $GLOBALS['basedir'] . 'pages/errors/404.php';
-	} else {
-		header($_SERVER["SERVER_PROTOCOL"]." 403 Forbidden");
-		header("Status: 403 Forbidden");
-		$_SERVER['REDIRECT_STATUS'] = 403;
-		return $GLOBALS['basedir'] . 'pages/errors/403.php';
-	}
-}
-
-function getRequestPage($request = '') {
-	if($request == '' or $request == '/') {
-		return array(array(), array());
-	}
-	$rArray = explode('/', $request);
-	$args = array();
-	while(!file_exists($file = 'pages/' . implode('/', $rArray) . '.php') and $rArray != NULL) {
-		array_unshift($args, array_pop($rArray));
-	}
-	return array($rArray, $args);
-}
-
-include 'libs/utils.php';
-
-$baseurl = trim(full_url($_SERVER), '/') . '/';
-$basedir = dirname($_SERVER['SCRIPT_FILENAME']) . '/';
-
-$dirty_req = preg_replace('/(\?.*)/', '', $_SERVER['REQUEST_URI']);
-
-$dn = dirname($_SERVER['SCRIPT_NAME']);
-$dirty_req = substr($dirty_req, strlen($dn));
-
-$dirty_req = trim($dirty_req, '/');
-if(strlen($dirty_req) > 0) {
-	$baseurl = substr(trim(preg_replace('/(\?.*)/', '', $baseurl), '/'), 0, -strlen($dirty_req));
-} else {
-	$baseurl = preg_replace('/(\?.*)/', '', $baseurl);
-}
-
-if(strlen($dirty_req) > 0) {
-	if(substr($_SERVER['REQUEST_URI'], -1) === '/') {
-		$redirect_url = trim(full_url($_SERVER), '/');
-		header("HTTP/1.1 301 Moved Permanently"); 
-		header("Location: $redirect_url");
-		die;
-	}
-}
-
-$request = $dirty_req;
-
-ob_start();
-
-$r = getRequestPage($request);
-$GLOBALS['args'] = $r[1];
-
-if(empty($r[0])) {
-	if(empty($r[1])) {
-		include $basedir . 'pages/home.php';
-	} else {
-		include error(404);
-	}
-} else {
-	include $basedir.'pages/'.implode('/', $r[0]).'.php';
-}
-
-$html = ob_get_clean();
-
-echo $html;

+ 0 - 368
js/H5F.js

@@ -1,368 +0,0 @@
-/*! H5F
-* https://github.com/ryanseddon/H5F/
-* Copyright (c) Ryan Seddon | Licensed MIT */
-
-(function (root, factory) {
-    if (typeof define === 'function' && define.amd) {
-        // AMD. Register as an anonymous module.
-        define(factory);
-    } else if (typeof module === 'object' && module.exports)  {
-        // CommonJS
-        module.exports = factory();
-    } else {
-        // Browser globals
-        root.H5F = factory();
-    }
-}(this, function () {
-
-    var d = document,
-        field = d.createElement("input"),
-        emailPatt = /^[a-zA-Z0-9.!#$%&'*+-\/=?\^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/,
-        urlPatt = /[a-z][\-\.+a-z]*:\/\//i,
-        nodes = /^(input|select|textarea)$/i,
-        isSubmit, bypassSubmit, usrPatt, curEvt, args,
-        // Methods
-        setup, validation, validity, checkField, bypassChecks, checkValidity, setCustomValidity, support, pattern, placeholder, range, required, valueMissing, listen, unlisten, preventActions, getTarget, addClass, removeClass, isHostMethod, isSiblingChecked;
-
-    setup = function(form, settings) {
-        var isCollection = !form.nodeType || false;
-
-        var opts = {
-            validClass : "valid",
-            invalidClass : "error",
-            requiredClass : "required",
-            placeholderClass : "placeholder",
-            onSubmit : Function.prototype,
-            onInvalid : Function.prototype
-        };
-
-        if(typeof settings === "object") {
-            for (var i in opts) {
-                if(typeof settings[i] === "undefined") { settings[i] = opts[i]; }
-            }
-        }
-
-        args = settings || opts;
-
-        if(isCollection) {
-            for(var k=0,len=form.length;k<len;k++) {
-                validation(form[k]);
-            }
-        } else {
-            validation(form);
-        }
-    };
-
-    validation = function(form) {
-        var f = form.elements,
-            flen = f.length,
-            isRequired,
-            noValidate = !!(form.attributes["novalidate"]);
-
-        listen(form,"invalid",checkField,true);
-        listen(form,"blur",checkField,true);
-        listen(form,"input",checkField,true);
-        listen(form,"keyup",checkField,true);
-        listen(form,"focus",checkField,true);
-        listen(form,"change",checkField,true);
-        listen(form,"click",bypassChecks,true);
-
-        listen(form,"submit",function(e){
-            isSubmit = true;
-            if(!bypassSubmit && !noValidate && !form.checkValidity()) {
-                preventActions(e);
-                return;
-            }
-            args.onSubmit.call(form, e);
-        },false);
-
-        if(!support()) {
-            form.checkValidity = function() { return checkValidity(form); };
-
-            while(flen--) {
-                isRequired = !!(f[flen].attributes["required"]);
-                // Firefox includes fieldsets inside elements nodelist so we filter it out.
-                if(f[flen].nodeName.toLowerCase() !== "fieldset") {
-                    validity(f[flen]); // Add validity object to field
-                }
-            }
-        }
-    };
-    validity = function(el) {
-        var elem = el,
-            missing = valueMissing(elem),
-            attrs = {
-                type: elem.getAttribute("type"),
-                pattern: elem.getAttribute("pattern"),
-                placeholder: elem.getAttribute("placeholder")
-            },
-            isType = /^(email|url)$/i,
-            evt = /^(input|keyup)$/i,
-            fType = ((isType.test(attrs.type)) ? attrs.type : ((attrs.pattern) ? attrs.pattern : false)),
-            patt = pattern(elem,fType),
-            step = range(elem,"step"),
-            min = range(elem,"min"),
-            max = range(elem,"max"),
-            customError = !( elem.validationMessage === "" || elem.validationMessage === undefined );
-
-        elem.checkValidity = function() { return checkValidity.call(this,elem); };
-        elem.setCustomValidity = function(msg) { setCustomValidity.call(elem,msg); };
-
-        elem.validity = {
-            valueMissing: missing,
-            patternMismatch: patt,
-            rangeUnderflow: min,
-            rangeOverflow: max,
-            stepMismatch: step,
-            customError: customError,
-            valid: (!missing && !patt && !step && !min && !max && !customError)
-        };
-
-        if(attrs.placeholder && !evt.test(curEvt)) { placeholder(elem); }
-    };
-    checkField = function(e) {
-        var el = getTarget(e) || e, // checkValidity method passes element not event
-            events = /^(input|keyup|focusin|focus|change)$/i,
-            ignoredTypes = /^(submit|image|button|reset)$/i,
-            specialTypes = /^(checkbox|radio)$/i,
-            checkForm = true;
-
-        if(nodes.test(el.nodeName) && !(ignoredTypes.test(el.type) || ignoredTypes.test(el.nodeName))) {
-            curEvt = e.type;
-
-            if(!support()) {
-                validity(el);
-            }
-
-            if(el.validity.valid && (el.value !== "" || specialTypes.test(el.type)) || (el.value !== el.getAttribute("placeholder") && el.validity.valid)) {
-                removeClass(el,[args.invalidClass,args.requiredClass]);
-                addClass(el,args.validClass);
-            } else if(!events.test(curEvt)) {
-                if(el.validity.valueMissing) {
-                    removeClass(el,[args.invalidClass,args.validClass]);
-                    addClass(el,args.requiredClass);
-                } else if(!el.validity.valid) {
-                    removeClass(el,[args.validClass,args.requiredClass]);
-                    addClass(el,args.invalidClass);
-                }
-            } else if(el.validity.valueMissing) {
-                removeClass(el,[args.requiredClass,args.invalidClass,args.validClass]);
-            }
-            if(curEvt === "input" && checkForm) {
-                // If input is triggered remove the keyup event
-                unlisten(el.form,"keyup",checkField,true);
-                checkForm = false;
-            }
-        }
-    };
-    checkValidity = function(el) {
-        var f, ff, isDisabled, isRequired, hasPattern, invalid = false;
-
-        if(el.nodeName.toLowerCase() === "form") {
-            f = el.elements;
-
-            for(var i = 0,len = f.length;i < len;i++) {
-                ff = f[i];
-
-                isDisabled = !!(ff.attributes["disabled"]);
-                isRequired = !!(ff.attributes["required"]);
-                hasPattern = !!(ff.attributes["pattern"]);
-
-                if(ff.nodeName.toLowerCase() !== "fieldset" && !isDisabled && (isRequired || hasPattern && isRequired)) {
-                    checkField(ff);
-                    if(!ff.validity.valid && !invalid) {
-                        if(isSubmit) { // If it's not a submit event the field shouldn't be focused
-                            ff.focus();
-                        }
-                        invalid = true;
-                        args.onInvalid.call(el, ff);
-                    }
-                }
-            }
-            return !invalid;
-        } else {
-            checkField(el);
-            return el.validity.valid;
-        }
-    };
-    setCustomValidity = function(msg) {
-        var el = this;
-
-        el.validationMessage = msg;
-    };
-
-    bypassChecks = function(e) {
-        // handle formnovalidate attribute
-        var el = getTarget(e);
-
-        if(el.attributes["formnovalidate"] && el.type === "submit") {
-            bypassSubmit = true;
-        }
-    };
-
-    support = function() {
-        return (isHostMethod(field,"validity") && isHostMethod(field,"checkValidity"));
-    };
-
-    // Create helper methods to emulate attributes in older browsers
-    pattern = function(el, type) {
-        if(type === "email") {
-            return !emailPatt.test(el.value);
-        } else if(type === "url") {
-            return !urlPatt.test(el.value);
-        } else if(!type) {
-            return false;
-        } else {
-            var placeholder = el.getAttribute("placeholder"),
-                val = el.value;
-
-            usrPatt = new RegExp('^(?:' + type + ')$');
-
-            if(val === placeholder) {
-                return false;
-            } else if(val === "") {
-                return false;
-            } else {
-                return !usrPatt.test(el.value);
-            }
-        }
-    };
-    placeholder = function(el) {
-        var attrs = { placeholder: el.getAttribute("placeholder") },
-            focus = /^(focus|focusin|submit)$/i,
-            node = /^(input|textarea)$/i,
-            ignoredType = /^password$/i,
-            isNative = !!("placeholder" in field);
-
-        if(!isNative && node.test(el.nodeName) && !ignoredType.test(el.type)) {
-            if(el.value === "" && !focus.test(curEvt)) {
-                el.value = attrs.placeholder;
-                listen(el.form,'submit', function () {
-                  curEvt = 'submit';
-                  placeholder(el);
-                }, true);
-                addClass(el,args.placeholderClass);
-            } else if(el.value === attrs.placeholder && focus.test(curEvt)) {
-                el.value = "";
-                removeClass(el,args.placeholderClass);
-            }
-        }
-    };
-    range = function(el, type) {
-        // Emulate min, max and step
-        var min = parseInt(el.getAttribute("min"),10) || 0,
-            max = parseInt(el.getAttribute("max"),10) || false,
-            step = parseInt(el.getAttribute("step"),10) || 1,
-            val = parseInt(el.value,10),
-            mismatch = (val-min)%step;
-
-        if(!valueMissing(el) && !isNaN(val)) {
-            if(type === "step") {
-                return (el.getAttribute("step")) ? (mismatch !== 0) : false;
-            } else if(type === "min") {
-                return (el.getAttribute("min")) ? (val < min) : false;
-            } else if(type === "max") {
-                return (el.getAttribute("max")) ? (val > max) : false;
-            }
-        } else if(el.getAttribute("type") === "number") {
-            return true;
-        } else {
-            return false;
-        }
-    };
-    required = function(el) {
-        var required = !!(el.attributes["required"]);
-
-        return (required) ? valueMissing(el) : false;
-    };
-    valueMissing = function(el) {
-        var placeholder = el.getAttribute("placeholder"),
-            specialTypes = /^(checkbox|radio)$/i,
-            isRequired = !!(el.attributes["required"]);
-        return !!(isRequired && (el.value === "" || el.value === placeholder || (specialTypes.test(el.type) && !isSiblingChecked(el))));
-    };
-
-    /* Util methods */
-    listen = function (node,type,fn,capture) {
-        if(isHostMethod(window,"addEventListener")) {
-            /* FF & Other Browsers */
-            node.addEventListener( type, fn, capture );
-        } else if(isHostMethod(window,"attachEvent") && typeof window.event !== "undefined") {
-            /* Internet Explorer way */
-            if(type === "blur") {
-                type = "focusout";
-            } else if(type === "focus") {
-                type = "focusin";
-            }
-            node.attachEvent( "on" + type, fn );
-        }
-    };
-    unlisten = function (node,type,fn,capture) {
-        if(isHostMethod(window,"removeEventListener")) {
-            /* FF & Other Browsers */
-            node.removeEventListener( type, fn, capture );
-        } else if(isHostMethod(window,"detachEvent") && typeof window.event !== "undefined") {
-            /* Internet Explorer way */
-            node.detachEvent( "on" + type, fn );
-        }
-    };
-    preventActions = function (evt) {
-        evt = evt || window.event;
-
-        if(evt.stopPropagation && evt.preventDefault) {
-            evt.stopPropagation();
-            evt.preventDefault();
-        } else {
-            evt.cancelBubble = true;
-            evt.returnValue = false;
-        }
-    };
-    getTarget = function (evt) {
-        evt = evt || window.event;
-        return evt.target || evt.srcElement;
-    };
-    addClass = function (e,c) {
-        var re;
-        if (!e.className) {
-            e.className = c;
-        }
-        else {
-            re = new RegExp('(^|\\s)' + c + '(\\s|$)');
-            if (!re.test(e.className)) { e.className += ' ' + c; }
-        }
-    };
-    removeClass = function (e,c) {
-        var re, m, arr = (typeof c === "object") ? c.length : 1, len = arr;
-        if (e.className) {
-            if (e.className === c) {
-                e.className = '';
-            } else {
-                while(arr--) {
-                    re = new RegExp('(^|\\s)' + ((len > 1) ? c[arr] : c) + '(\\s|$)');
-                    m = e.className.match(re);
-                    if (m && m.length === 3) { e.className = e.className.replace(re, (m[1] && m[2])?' ':''); }
-                }
-            }
-        }
-    };
-    isHostMethod = function(o, m) {
-        var t = typeof o[m], reFeaturedMethod = new RegExp('^function|object$', 'i');
-        return !!((reFeaturedMethod.test(t) && o[m]) || t === 'unknown');
-    };
-    /* Checking if one of the radio siblings is checked */
-    isSiblingChecked = function(el) {
-        var siblings = document.getElementsByName(el.name);
-        for(var i=0; i<siblings.length; i++){
-            if(siblings[i].checked){
-                return true;
-            }
-        }
-        return false;
-    };
-
-    // Since all methods are only used internally no need to expose globally
-    return {
-        setup: setup
-    };
-
-}));

+ 0 - 19
js/contact.js

@@ -1,19 +0,0 @@
-function validate() {
-	var valid = true;
-	if($('#senderNameBox')[0].checkValidity() && $('#senderEmailBox')[0].checkValidity() && $('#messageBox')[0].checkValidity()) {
-		$('#submitButton').prop('disabled', false);
-	} else {
-		$('#submitButton').prop('disabled', true);
-	}
-}
-
-$(function() {
-	var form = $('#theContactForm');
-	H5F.setup([form[0]], {
-		validClass: 'valid',
-		invalidClass: 'invalid',
-		requiredClass: 'required'
-	});
-
-	$.each([$('#senderNameBox'), $('#senderEmailBox'), $('#messageBox')], function(i,o) { $(o).keyup(validate) });
-});

+ 0 - 322
js/html5.min.js

@@ -1,322 +0,0 @@
-/**
-* @preserve HTML5 Shiv 3.7.2 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
-*/
-;(function(window, document) {
-/*jshint evil:true */
-  /** version */
-  var version = '3.7.2';
-
-  /** Preset options */
-  var options = window.html5 || {};
-
-  /** Used to skip problem elements */
-  var reSkip = /^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i;
-
-  /** Not all elements can be cloned in IE **/
-  var saveClones = /^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i;
-
-  /** Detect whether the browser supports default html5 styles */
-  var supportsHtml5Styles;
-
-  /** Name of the expando, to work with multiple documents or to re-shiv one document */
-  var expando = '_html5shiv';
-
-  /** The id for the the documents expando */
-  var expanID = 0;
-
-  /** Cached data for each document */
-  var expandoData = {};
-
-  /** Detect whether the browser supports unknown elements */
-  var supportsUnknownElements;
-
-  (function() {
-    try {
-        var a = document.createElement('a');
-        a.innerHTML = '<xyz></xyz>';
-        //if the hidden property is implemented we can assume, that the browser supports basic HTML5 Styles
-        supportsHtml5Styles = ('hidden' in a);
-
-        supportsUnknownElements = a.childNodes.length == 1 || (function() {
-          // assign a false positive if unable to shiv
-          (document.createElement)('a');
-          var frag = document.createDocumentFragment();
-          return (
-            typeof frag.cloneNode == 'undefined' ||
-            typeof frag.createDocumentFragment == 'undefined' ||
-            typeof frag.createElement == 'undefined'
-          );
-        }());
-    } catch(e) {
-      // assign a false positive if detection fails => unable to shiv
-      supportsHtml5Styles = true;
-      supportsUnknownElements = true;
-    }
-
-  }());
-
-  /*--------------------------------------------------------------------------*/
-
-  /**
-   * Creates a style sheet with the given CSS text and adds it to the document.
-   * @private
-   * @param {Document} ownerDocument The document.
-   * @param {String} cssText The CSS text.
-   * @returns {StyleSheet} The style element.
-   */
-  function addStyleSheet(ownerDocument, cssText) {
-    var p = ownerDocument.createElement('p'),
-        parent = ownerDocument.getElementsByTagName('head')[0] || ownerDocument.documentElement;
-
-    p.innerHTML = 'x<style>' + cssText + '</style>';
-    return parent.insertBefore(p.lastChild, parent.firstChild);
-  }
-
-  /**
-   * Returns the value of `html5.elements` as an array.
-   * @private
-   * @returns {Array} An array of shived element node names.
-   */
-  function getElements() {
-    var elements = html5.elements;
-    return typeof elements == 'string' ? elements.split(' ') : elements;
-  }
-
-  /**
-   * Extends the built-in list of html5 elements
-   * @memberOf html5
-   * @param {String|Array} newElements whitespace separated list or array of new element names to shiv
-   * @param {Document} ownerDocument The context document.
-   */
-  function addElements(newElements, ownerDocument) {
-    var elements = html5.elements;
-    if(typeof elements != 'string'){
-      elements = elements.join(' ');
-    }
-    if(typeof newElements != 'string'){
-      newElements = newElements.join(' ');
-    }
-    html5.elements = elements +' '+ newElements;
-    shivDocument(ownerDocument);
-  }
-
-   /**
-   * Returns the data associated to the given document
-   * @private
-   * @param {Document} ownerDocument The document.
-   * @returns {Object} An object of data.
-   */
-  function getExpandoData(ownerDocument) {
-    var data = expandoData[ownerDocument[expando]];
-    if (!data) {
-        data = {};
-        expanID++;
-        ownerDocument[expando] = expanID;
-        expandoData[expanID] = data;
-    }
-    return data;
-  }
-
-  /**
-   * returns a shived element for the given nodeName and document
-   * @memberOf html5
-   * @param {String} nodeName name of the element
-   * @param {Document} ownerDocument The context document.
-   * @returns {Object} The shived element.
-   */
-  function createElement(nodeName, ownerDocument, data){
-    if (!ownerDocument) {
-        ownerDocument = document;
-    }
-    if(supportsUnknownElements){
-        return ownerDocument.createElement(nodeName);
-    }
-    if (!data) {
-        data = getExpandoData(ownerDocument);
-    }
-    var node;
-
-    if (data.cache[nodeName]) {
-        node = data.cache[nodeName].cloneNode();
-    } else if (saveClones.test(nodeName)) {
-        node = (data.cache[nodeName] = data.createElem(nodeName)).cloneNode();
-    } else {
-        node = data.createElem(nodeName);
-    }
-
-    // Avoid adding some elements to fragments in IE < 9 because
-    // * Attributes like `name` or `type` cannot be set/changed once an element
-    //   is inserted into a document/fragment
-    // * Link elements with `src` attributes that are inaccessible, as with
-    //   a 403 response, will cause the tab/window to crash
-    // * Script elements appended to fragments will execute when their `src`
-    //   or `text` property is set
-    return node.canHaveChildren && !reSkip.test(nodeName) && !node.tagUrn ? data.frag.appendChild(node) : node;
-  }
-
-  /**
-   * returns a shived DocumentFragment for the given document
-   * @memberOf html5
-   * @param {Document} ownerDocument The context document.
-   * @returns {Object} The shived DocumentFragment.
-   */
-  function createDocumentFragment(ownerDocument, data){
-    if (!ownerDocument) {
-        ownerDocument = document;
-    }
-    if(supportsUnknownElements){
-        return ownerDocument.createDocumentFragment();
-    }
-    data = data || getExpandoData(ownerDocument);
-    var clone = data.frag.cloneNode(),
-        i = 0,
-        elems = getElements(),
-        l = elems.length;
-    for(;i<l;i++){
-        clone.createElement(elems[i]);
-    }
-    return clone;
-  }
-
-  /**
-   * Shivs the `createElement` and `createDocumentFragment` methods of the document.
-   * @private
-   * @param {Document|DocumentFragment} ownerDocument The document.
-   * @param {Object} data of the document.
-   */
-  function shivMethods(ownerDocument, data) {
-    if (!data.cache) {
-        data.cache = {};
-        data.createElem = ownerDocument.createElement;
-        data.createFrag = ownerDocument.createDocumentFragment;
-        data.frag = data.createFrag();
-    }
-
-
-    ownerDocument.createElement = function(nodeName) {
-      //abort shiv
-      if (!html5.shivMethods) {
-          return data.createElem(nodeName);
-      }
-      return createElement(nodeName, ownerDocument, data);
-    };
-
-    ownerDocument.createDocumentFragment = Function('h,f', 'return function(){' +
-      'var n=f.cloneNode(),c=n.createElement;' +
-      'h.shivMethods&&(' +
-        // unroll the `createElement` calls
-        getElements().join().replace(/[\w\-:]+/g, function(nodeName) {
-          data.createElem(nodeName);
-          data.frag.createElement(nodeName);
-          return 'c("' + nodeName + '")';
-        }) +
-      ');return n}'
-    )(html5, data.frag);
-  }
-
-  /*--------------------------------------------------------------------------*/
-
-  /**
-   * Shivs the given document.
-   * @memberOf html5
-   * @param {Document} ownerDocument The document to shiv.
-   * @returns {Document} The shived document.
-   */
-  function shivDocument(ownerDocument) {
-    if (!ownerDocument) {
-        ownerDocument = document;
-    }
-    var data = getExpandoData(ownerDocument);
-
-    if (html5.shivCSS && !supportsHtml5Styles && !data.hasCSS) {
-      data.hasCSS = !!addStyleSheet(ownerDocument,
-        // corrects block display not defined in IE6/7/8/9
-        'article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}' +
-        // adds styling not present in IE6/7/8/9
-        'mark{background:#FF0;color:#000}' +
-        // hides non-rendered elements
-        'template{display:none}'
-      );
-    }
-    if (!supportsUnknownElements) {
-      shivMethods(ownerDocument, data);
-    }
-    return ownerDocument;
-  }
-
-  /*--------------------------------------------------------------------------*/
-
-  /**
-   * The `html5` object is exposed so that more elements can be shived and
-   * existing shiving can be detected on iframes.
-   * @type Object
-   * @example
-   *
-   * // options can be changed before the script is included
-   * html5 = { 'elements': 'mark section', 'shivCSS': false, 'shivMethods': false };
-   */
-  var html5 = {
-
-    /**
-     * An array or space separated string of node names of the elements to shiv.
-     * @memberOf html5
-     * @type Array|String
-     */
-    'elements': options.elements || 'abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video',
-
-    /**
-     * current version of html5shiv
-     */
-    'version': version,
-
-    /**
-     * A flag to indicate that the HTML5 style sheet should be inserted.
-     * @memberOf html5
-     * @type Boolean
-     */
-    'shivCSS': (options.shivCSS !== false),
-
-    /**
-     * Is equal to true if a browser supports creating unknown/HTML5 elements
-     * @memberOf html5
-     * @type boolean
-     */
-    'supportsUnknownElements': supportsUnknownElements,
-
-    /**
-     * A flag to indicate that the document's `createElement` and `createDocumentFragment`
-     * methods should be overwritten.
-     * @memberOf html5
-     * @type Boolean
-     */
-    'shivMethods': (options.shivMethods !== false),
-
-    /**
-     * A string to describe the type of `html5` object ("default" or "default print").
-     * @memberOf html5
-     * @type String
-     */
-    'type': 'default',
-
-    // shivs the document according to the specified `html5` object options
-    'shivDocument': shivDocument,
-
-    //creates a shived element
-    createElement: createElement,
-
-    //creates a shived documentFragment
-    createDocumentFragment: createDocumentFragment,
-
-    //extends list of elements
-    addElements: addElements
-  };
-
-  /*--------------------------------------------------------------------------*/
-
-  // expose html5
-  window.html5 = html5;
-
-  // shiv the document
-  shivDocument(document);
-
-}(this, document));

File diff suppressed because it is too large
+ 0 - 1
js/jquery.min.js


+ 0 - 147
js/js.cookie.js

@@ -1,147 +0,0 @@
-/*!
- * Javascript Cookie v1.5.1
- * https://github.com/js-cookie/js-cookie
- *
- * Copyright 2006, 2014 Klaus Hartl
- * Released under the MIT license
- */
-(function (factory) {
-	var jQuery;
-	if (typeof define === 'function' && define.amd) {
-		// AMD (Register as an anonymous module)
-		define(['jquery'], factory);
-	} else if (typeof exports === 'object') {
-		// Node/CommonJS
-		try {
-			jQuery = require('jquery');
-		} catch(e) {}
-		module.exports = factory(jQuery);
-	} else {
-		// Browser globals
-		var _OldCookies = window.Cookies;
-		var api = window.Cookies = factory(window.jQuery);
-		api.noConflict = function() {
-			window.Cookies = _OldCookies;
-			return api;
-		};
-	}
-}(function ($) {
-
-	var pluses = /\+/g;
-
-	function encode(s) {
-		return api.raw ? s : encodeURIComponent(s);
-	}
-
-	function decode(s) {
-		return api.raw ? s : decodeURIComponent(s);
-	}
-
-	function stringifyCookieValue(value) {
-		return encode(api.json ? JSON.stringify(value) : String(value));
-	}
-
-	function parseCookieValue(s) {
-		if (s.indexOf('"') === 0) {
-			// This is a quoted cookie as according to RFC2068, unescape...
-			s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
-		}
-
-		try {
-			// Replace server-side written pluses with spaces.
-			// If we can't decode the cookie, ignore it, it's unusable.
-			// If we can't parse the cookie, ignore it, it's unusable.
-			s = decodeURIComponent(s.replace(pluses, ' '));
-			return api.json ? JSON.parse(s) : s;
-		} catch(e) {}
-	}
-
-	function read(s, converter) {
-		var value = api.raw ? s : parseCookieValue(s);
-		return isFunction(converter) ? converter(value) : value;
-	}
-
-	function extend() {
-		var key, options;
-		var i = 0;
-		var result = {};
-		for (; i < arguments.length; i++) {
-			options = arguments[ i ];
-			for (key in options) {
-				result[key] = options[key];
-			}
-		}
-		return result;
-	}
-
-	function isFunction(obj) {
-		return Object.prototype.toString.call(obj) === '[object Function]';
-	}
-
-	var api = function (key, value, options) {
-
-		// Write
-
-		if (arguments.length > 1 && !isFunction(value)) {
-			options = extend(api.defaults, options);
-
-			if (typeof options.expires === 'number') {
-				var days = options.expires, t = options.expires = new Date();
-				t.setMilliseconds(t.getMilliseconds() + days * 864e+5);
-			}
-
-			return (document.cookie = [
-				encode(key), '=', stringifyCookieValue(value),
-				options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
-				options.path    ? '; path=' + options.path : '',
-				options.domain  ? '; domain=' + options.domain : '',
-				options.secure  ? '; secure' : ''
-			].join(''));
-		}
-
-		// Read
-
-		var result = key ? undefined : {},
-			// To prevent the for loop in the first place assign an empty array
-			// in case there are no cookies at all. Also prevents odd result when
-			// calling "get()".
-			cookies = document.cookie ? document.cookie.split('; ') : [],
-			i = 0,
-			l = cookies.length;
-
-		for (; i < l; i++) {
-			var parts = cookies[i].split('='),
-				name = decode(parts.shift()),
-				cookie = parts.join('=');
-
-			if (key === name) {
-				// If second argument (value) is a function it's a converter...
-				result = read(cookie, value);
-				break;
-			}
-
-			// Prevent storing a cookie that we couldn't decode.
-			if (!key && (cookie = read(cookie)) !== undefined) {
-				result[name] = cookie;
-			}
-		}
-
-		return result;
-	};
-
-	api.get = api.set = api;
-	api.defaults = {};
-
-	api.remove = function (key, options) {
-		// Must not alter options, thus extending a fresh object...
-		api(key, '', extend(options, { expires: -1 }));
-		return !api(key);
-	};
-
-	if ( $ ) {
-		$.cookie = api;
-		$.removeCookie = api.remove;
-	}
-
-	return api;
-}));

File diff suppressed because it is too large
+ 0 - 3
js/picturefill.min.js


+ 0 - 77
js/scripts.js

@@ -1,77 +0,0 @@
-// var doTheScroll = true;
-
-function debugRow(y, height) {
-	if(y === undefined) y = 0;
-	if(height === undefined) height = 1;	
-	$('body').append('<div class="debugrow" style="top: '+y+'px; height: '+height+'px; background: blue; position: absolute; width: 100%; opacity: 0.5;"></div>');
-}
-
-$(function() {
-	// EU COOKIE MONSTER
-	// var eupref = Cookies.get('eu-disable');
-	// if(eupref == undefined) {
-	// 	var cmaside = $('<aside id="cookie_monster">'+
-	// 					'	<div class="content">'+
-	// 					'		<p>Hello, did you know that I am using <strong>cookies</strong> in this website? I promise they are not tracking you, but I can disable them if you want.</p>'+
-	// 					'		<button class="button yes" type="button" data-disable="true">Disable them</button><button class="button no" data-disable="false">It\'s ok</button>'+
-	// 					'	</div>'+
-	// 					'</aside>');
-	// 	$('#top_banner').prepend(cmaside)
-	// 	$('#cookie_monster button').click(function(e) {
-	// 		t = $(e.target);
-	// 		if(t.data('disable')) {
-	// 			Cookies.set('eu-disable', true, { expires: 365 });
-	// 		} else {
-	// 			Cookies.set('eu-disable', false, { expires: 365 });
-	// 		}
-	// 		cmaside.fadeOut();
-	// 	});
-	// } else {
-	// 	Cookies.set('eu-disable', Cookies.get('eu-disable'), {expires: 365});
-	// 	if(Cookies.get('eu-disable') == "true") {
-	// 		window['ga-disable-UA-1176762-5'] = true;
-	// 	} else {
-	// 		window['ga-disable-UA-1176762-5'] = false;
-	// 	}
-	// }
-	// EU COOKIE MONSTER
-	
-	if(screen.width > 1024 && screen.height > 768 && window.devicePixelRatio > 1) {
-		$('[data-hq]').each(function() {
-			t = $(this);
-			if(t.is('img')) {
-				t.attr('src', t.data('hq'));
-			} else {
-				t.css('background-image', 'url(' + t.data('hq') + ')');
-			}
-		});
-	}
-	
-	baseline = 28;//parseFloat($('body').data('baseline'));
-
-	$('figure').each(function(i, e) {
-		figure = $(e);
-		realTop = figure.offset().top;
-		// debugRow(realTop);
-
-		realBottom = realTop + figure.height() + parseFloat(figure.css('margin-bottom'));
-		// debugRow(realBottom);
-
-		gap = baseline - ((realBottom - $('article.body').offset().top) % baseline);
-		figure.css('margin-top', '+=' + Math.floor(gap/2.0) + 'px');
-		figure.css('margin-bottom', '+=' + Math.floor(gap/2.0) + 'px');
-	});
-
-	/**
-	h = $(document).height();
-	b = 28;
-	c = $('#content_box').offset().top + b;
-	$('body').append('<div id="baseline_overlay" style="position: absolute; top: '+c+'px; width: 100%; height: ' + h + 'px; opacity: 0.4;"></div>');
-	bs = $('#baseline_overlay');
-	cl = 0;
-	while(cl < h) {
-		bs.append('<div style="position: absolute; top: ' +cl+ 'px; height: 1px; background: red; width: 100%;"></div>');
-		cl += b;
-	}
-	/**/
-});

+ 29 - 0
layouts/default.vue

@@ -0,0 +1,29 @@
+<template>
+    <div>
+        <!-- <Breakpoint /> -->
+        <TopBanner>
+            <template #headline><slot name="headline">Default Headline from layout</slot></template>
+            <template #tagline><slot name="tagline">Default Tagline from layout</slot></template>
+        </TopBanner>
+        <div id="content_box" :class="`flex flex-col bg-white pt-12 sm:pt-24`">
+            <CookieMonster />
+            <MainMenu />
+            <slot />
+        </div>
+        <Footer />
+    </div>
+</template>
+
+<style lang="pcss">
+article.body h2 {
+    @apply mt-0;
+}
+</style>
+
+<script lang="ts" setup>
+useHead({
+    bodyAttrs: {
+        class: `bg-black ${String(useRoute().name)}`
+    }
+})
+</script>

+ 0 - 46
libs/utils.php

@@ -1,46 +0,0 @@
-<?php
-
-function isSuperGlobal($name){
-    switch($name){
-        case 'GLOBALS':
-        case '_SERVER':
-        case '_GET':
-        case '_POST':
-        case '_FILES':
-        case '_COOKIE':
-        case '_SESSION':
-        case '_REQUEST':
-        case '_ENV':        return true;    break;
-        default:            return false;   break;
-    }
-}
-
-function i($i = '') {
-	foreach($GLOBALS as $key=>$val){
-	    if(!isSuperGlobal($key))                     
-	        global $$key;
-	}
-	
-	ob_start();
-	global $basedir;
-	include $basedir . 'pages/' . $i . '.php';
-	return ob_get_clean();
-}
-
-function url($url = '') {
-	global $baseurl;
-	return $baseurl . $url;
-}
-
-function url_origin($s, $use_forwarded_host=false) {
-    $ssl = (!empty($s['HTTPS']) && $s['HTTPS'] == 'on') ? true:false;
-    $sp = strtolower($s['SERVER_PROTOCOL']);
-    $protocol = substr($sp, 0, strpos($sp, '/')) . (($ssl) ? 's' : '');
-    $port = $s['SERVER_PORT'];
-    $port = ((!$ssl && $port=='80') || ($ssl && $port=='443')) ? '' : ':'.$port;
-    $host = ($use_forwarded_host && isset($s['HTTP_X_FORWARDED_HOST'])) ? $s['HTTP_X_FORWARDED_HOST'] : (isset($s['HTTP_HOST']) ? $s['HTTP_HOST'] : $s['SERVER_NAME']);
-    return $protocol . '://' . $host;
-}
-function full_url($s, $use_forwarded_host=false) {
-    return url_origin($s, $use_forwarded_host) . $s['REQUEST_URI'];
-}

+ 10 - 0
middleware/matomoRoutes.global.ts

@@ -0,0 +1,10 @@
+import { ConsentStore } from "~~/types";
+
+export default defineNuxtRouteMiddleware(_to => {
+    const consentStore = useCookie('consent-store').value as ConsentStore;
+    const analytics = consentStore?.analytics;
+    
+    if (process.client && analytics && window._paq) {
+        // window._paq.push(['trackPageView']);
+    } 
+});

+ 99 - 0
nuxt.config.ts

@@ -0,0 +1,99 @@
+import { defineNuxtConfig } from 'nuxt';
+
+const description = 'I am a Research Associate at the University of Cambridge, but I also do other things.';
+
+// https://v3.nuxtjs.org/api/configuration/nuxt.config
+export default defineNuxtConfig({
+    app: {
+        head: {
+            htmlAttrs: {
+                lang: 'en'
+            },
+            noscript: [
+                { children: `<div class="noscript">
+                        <center>Hi.</center>
+                        First of all: I am <strong>genuinely</strong> interested to know why you disabled JavaScript
+                        so <a href="/contact">please send me an e-mail</a> — no judgement.<br />
+                        Second: yes, I hate it too, but in this day and age JavaScript is kinda ubiquitous so I caved in.
+                        I am still making an effort but this site may look a bit weird and hardly usable without JS. Sorry…
+                    <noscript><p><img src="https://matomo.morpheu5.net/matomo.php?idsite=1&amp;rec=1" style="border:0;" alt="" /></p></noscript>
+                    </div>`
+                },
+            ],
+            meta: [
+                { charset: 'utf-8' },
+                { property: 'author', content: 'Andrea Franceschini' },
+                { property: 'description', content: description },
+                { property: 'DC.Creator', content: 'Andrea Franceschini' },
+                { property: 'DC.Description', content: description },
+                { property: 'DC.Rights', content: 'CC BY-NC-SA 4.0' },
+
+                { property: 'twitter:site', content: '@morpheu5' },
+                { property: 'twitter:creator', content: '@morpheu5' },
+                { property: 'twitter:description', content: description },
+                { property: 'twitter:card', content: 'summary_large_image' },
+                { property: 'twitter:image:src', content: 'https://andreafranceschini.org/images/colopic.jpg' },
+                { property: 'twitter:domain', content: 'https://andreafranceschini.org' },
+
+                { property: 'og:type', content: 'website' },
+                { property: 'og:image', content: 'https://andreafranceschini.org/images/colopic.jpg' },
+                { property: 'og:description', content: description },
+
+                { property: 'robots', content: 'All' },
+            ],
+            link: [
+                { rel: 'icon', type: 'image/svg+xml', href: '/images/favicon.svg' },
+                { rel: 'icon', type: 'image/png', href: '/images/favicon.png' }
+            ]
+        },
+    },
+    css: [
+        "~/assets/css/tailwind.css",
+        '@fortawesome/fontawesome-svg-core/styles.css',
+    ],
+    build: {
+        analyze: true,
+        transpile: [
+            'three'
+        ],
+        postcss: {
+            postcssOptions: {
+                map: process.dev || false,
+                plugins: {
+                    'postcss-import': {},
+                    'postcss-mixins': {},
+                    'tailwindcss/nesting': {},
+                    'tailwindcss': {},
+                    'autoprefixer': {},
+                    'postcss-preset-env': {},
+                    'cssnano': {},
+                },
+            }
+        },
+    },
+    modules: [
+        '@nuxtjs/google-fonts',
+        ['@pinia/nuxt', { autoImports: ['defineStore'] }],
+    ],
+    components: true,
+    telemetry: false,
+    ssr: true,
+    sourcemap: process.dev || false,
+    typescript: {
+        shim: false,
+        typeCheck: true,
+        strict: true,
+    },
+    googleFonts: {
+        families: {
+            Oswald: {
+                wght: [400, 600],
+            },
+            "Open Sans": {
+                wght: [400, 700],
+            },
+        },
+        download: true,
+        // overwriting: true,
+    }
+})

+ 0 - 77
oldcss/cookie.css

@@ -1,77 +0,0 @@
-/*
-Error: Undefined mixin 'breakpoint'.
-        on line 9 of /Users/morpheu5/web/af/css/sass/cookie.scss, in `breakpoint'
-        from line 9 of /Users/morpheu5/web/af/css/sass/cookie.scss
-
-4: 	width: 100%;
-5: 	background: hsla(0, 0, 100, 0.8);
-6: 	font-family: 'Open Sans', sans-serif;
-7: 	z-index: 255;
-8: 	
-9: 	@include breakpoint(min-width $bp-middle) {
-10: 		top: 0;
-11: 	}
-12: 	
-13: 	@include breakpoint(max-width $bp-middle) {
-14: 		bottom: 0;
-
-Backtrace:
-/Users/morpheu5/web/af/css/sass/cookie.scss:9:in `breakpoint'
-/Users/morpheu5/web/af/css/sass/cookie.scss:9
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/perform.rb:348:in `block in visit_mixin'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/stack.rb:98:in `block in with_mixin'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/stack.rb:115:in `with_frame'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/stack.rb:98:in `with_mixin'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/perform.rb:346:in `visit_mixin'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/base.rb:36:in `visit'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/perform.rb:158:in `block in visit'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/stack.rb:79:in `block in with_base'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/stack.rb:115:in `with_frame'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/stack.rb:79:in `with_base'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/perform.rb:158:in `visit'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/perform.rb:430:in `block (2 levels) in visit_rule'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/perform.rb:430:in `map'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/perform.rb:430:in `block in visit_rule'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/perform.rb:179:in `with_environment'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/perform.rb:428:in `visit_rule'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/base.rb:36:in `visit'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/perform.rb:158:in `block in visit'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/stack.rb:79:in `block in with_base'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/stack.rb:115:in `with_frame'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/stack.rb:79:in `with_base'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/perform.rb:158:in `visit'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/base.rb:52:in `block in visit_children'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/base.rb:52:in `map'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/base.rb:52:in `visit_children'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/perform.rb:167:in `block in visit_children'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/perform.rb:179:in `with_environment'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/perform.rb:166:in `visit_children'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/base.rb:36:in `block in visit'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/perform.rb:186:in `visit_root'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/base.rb:36:in `visit'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/perform.rb:157:in `visit'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/visitors/perform.rb:8:in `visit'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/root_node.rb:36:in `css_tree'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/tree/root_node.rb:20:in `render'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/engine.rb:268:in `render'
-/Library/Ruby/Gems/2.0.0/gems/compass-import-once-1.0.5/lib/compass/import-once/engine.rb:17:in `block in render'
-/Library/Ruby/Gems/2.0.0/gems/compass-import-once-1.0.5/lib/compass/import-once/engine.rb:29:in `with_import_scope'
-/Library/Ruby/Gems/2.0.0/gems/compass-import-once-1.0.5/lib/compass/import-once/engine.rb:16:in `render'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/plugin/compiler.rb:492:in `update_stylesheet'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/plugin/compiler.rb:215:in `block in update_stylesheets'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/plugin/compiler.rb:209:in `each'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/plugin/compiler.rb:209:in `update_stylesheets'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/plugin/compiler.rb:470:in `on_file_changed'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/lib/sass/plugin/compiler.rb:328:in `block in watch'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/vendor/listen/lib/listen/listener.rb:252:in `call'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/vendor/listen/lib/listen/listener.rb:252:in `on_change'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/vendor/listen/lib/listen/listener.rb:290:in `block in initialize_adapter'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/vendor/listen/lib/listen/adapter.rb:254:in `call'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/vendor/listen/lib/listen/adapter.rb:254:in `report_changes'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/vendor/listen/lib/listen/adapter.rb:323:in `poll_changed_directories'
-/Library/Ruby/Gems/2.0.0/gems/sass-3.4.13/vendor/listen/lib/listen/adapter.rb:299:in `block in start_poller'
-*/
-body:before {
-  white-space: pre;
-  font-family: monospace;
-  content: "Error: Undefined mixin 'breakpoint'.\A         on line 9 of /Users/morpheu5/web/af/css/sass/cookie.scss, in `breakpoint'\A         from line 9 of /Users/morpheu5/web/af/css/sass/cookie.scss\A \A 4: 	width: 100%;\A 5: 	background: hsla(0, 0, 100, 0.8);\A 6: 	font-family: 'Open Sans', sans-serif;\A 7: 	z-index: 255;\A 8: 	\A 9: 	@include breakpoint(min-width $bp-middle) {\A 10: 		top: 0;\A 11: 	}\A 12: 	\A 13: 	@include breakpoint(max-width $bp-middle) {\A 14: 		bottom: 0;"; }

+ 0 - 43
oldcss/sass/_base.scss

@@ -1,43 +0,0 @@
-@charset "utf-8";
-
-@import "susy";
-@import "compass/layout/stretching";
-@import "compass/css3/border-radius";
-@import "compass/css3/background-size";
-//@import "memo";
-@import "breakpoint";
-@import "compass/layout/grid-background";
-@import "compass/typography/vertical_rhythm";
-@import "compass/css3/transform";
-@import "compass/layout";
-@import "compass/css3/flexbox";
-@import "compass/css3/text-shadow";
-@import "compass/typography/text/nowrap";
-
-$corner-radius: 4px;
-
-$susy: (
-	container: 48em,
-	columns: 6,
-	gutters: 1/3
-);
-
-$bp-middle: 48em;
-$bp-small: 32em;
-$bp-tiny: 24em;
-
-@function em($px, $base: $base-font-size) {
-    @return ($px / $base) * 1em;
-}
-
-@mixin text-size-adjust($value: none) {
-  -webkit-text-size-adjust: $value; 
-  -moz-text-size-adjust: $value;
-  -ms-text-size-adjust: $value; 
-}
-
-$link-color: hsl(210, 50, 50);
-$abbr-color: scale-lightness(scale-saturation($link-color, 75%), -25%);
-
-$base-font-size: 16px;
-$base-line-height: $base-font-size * 1.75;

+ 0 - 102
oldcss/sass/_contact.scss

@@ -1,102 +0,0 @@
-.contact.page {
-	#content_box {
-		article {
-			section.links {
-				@include span(first 3);
-				
-				@include breakpoint(max-width $bp-middle) {
-					@include span(6)
-				}
-			}
-			
-			section.form {
-				@include span(last 3);
-				
-				@include breakpoint(max-width $bp-middle) {
-					@include span(full)
-				}
-				
-				p.contact_thanks {
-					background: hsl(120, 50, 95);
-					color: hsl(120, 80, 25);
-					padding: 0.5em $base-line-height/2;
-					@include border-radius($corner-radius);
-				}
-				
-				p.meanwhile {
-					padding: 0.5em $base-line-height/2;
-				}
-
-				.field {
-					display: block;
-					width: 100%;
-					padding-bottom: $base-line-height;
-					
-					label {
-						display: block;
-						color: adjust-lightness($link-color, 25);
-					}
-					
-					input, textarea {
-						@include border-radius($corner-radius);
-						padding: 0 0 0 0.25em;
-						display: block;
-						width: span(full);
-						height: $base-line-height;
-						font-size: em(16px);
-						font-family: "Open Sans", sans-serif;
-						border: 0;
-						border-bottom: 2px solid hsl(210, 50, 50);
-						background: hsl(210, 50, 98);
-						
-						@include breakpoint(max-width $bp-middle) {
-							width: span(full split);
-						}
-					}
-					
-					textarea {
-						min-height: 5*$base-line-height;
-						max-width: span(full);
-					}
-					
-					input:focus, textarea:focus {
-						outline: none;
-					}
-
-					input.invalid, textarea.invalid {
-						border-bottom: 2px solid hsl(210, 50, 50);
-						background: hsl(210, 50, 98);
-					}
-					
-					input.valid, textarea.valid {
-						border-bottom: 2px solid hsl(210, 50, 50);
-						background: white;
-					}
-				}
-				
-				.button {
-					text-align: right;
-					
-					button {
-						background: hsl(210, 50, 45);
-						font-weight: bold;
-						@include border-radius($corner-radius);
-						color: white;
-						border: 0;
-						font-size: 1em;
-						line-height: 2.5em;
-						padding: 0 0.75em 0.1em 0.5em;
-						
-						.fa {
-							padding-right: 0.25em;
-						}
-						
-						&:disabled {
-							background: adjust-lightness($link-color, 30);
-						}
-					}
-				}
-			}
-		}
-	}
-}

+ 0 - 25
oldcss/sass/_cv.scss

@@ -1,25 +0,0 @@
-.page.cv {
-	.macro {
-		margin-top: $base-line-height;
-		
-		&.first {
-			margin-top: 0;
-		}
-		
-		.body {
-			section {
-				margin-top: $base-line-height;
-				
-				&:first-child, &#full_cv {
-					margin-top: 0;
-				}
-				
-				section {
-					margin-top: 0;
-					padding-left: 1em;
-					border-left: 3px solid adjust-lightness($link-color, 40);
-				}
-			}
-		}
-	}
-}

+ 0 - 17
oldcss/sass/_development.scss

@@ -1,17 +0,0 @@
-.development.page {
-	#content_box {
-		article {
-			section.project {
-				@include breakpoint(max-width $bp-small) {
-					margin-top: $base-line-height/2;
-					border-top: 1px solid hsl(0,0,80);
-					padding-top: $base-line-height/2;
-					
-					h3 {
-						line-height: $base-line-height !important;
-					}
-				}
-			}
-		}
-	}
-}

+ 0 - 50
oldcss/sass/cookie.scss

@@ -1,50 +0,0 @@
-#cookie_monster {
-	position: fixed;
-	left: 0;
-	width: 100%;
-	background: hsla(0, 0, 100, 0.8);
-	font-family: 'Open Sans', sans-serif;
-	z-index: 255;
-	
-	@include breakpoint(min-width $bp-middle) {
-		top: 0;
-	}
-	
-	@include breakpoint(max-width $bp-middle) {
-		bottom: 0;
-		background: hsla(60, 75, 75, 0.8);
-	}
-	
-	.content {
-		@include container(48em);
-		margin: 0 auto;
-		color: black;
-		padding: em(6px) em(24px) em(3px) em(24px);
-		
-		strong {
-			font-weight: bold;
-		}
-		
-		.button {
-			display: block;
-			background: black;
-			color: white;
-			padding: em(6px) em(9px);
-			margin-top: em(6px);
-			margin-left: em(3px);
-			float: right;
-			@include border-radius($corner-radius);
-			border: 0;
-			font-size: $base-font-size;
-			
-			&.no {
-				background: hsla(150, 55, 45, 1);
-			}
-			
-			&.yes {
-				font-weight: bold;
-				background: hsla(330, 45, 55, 1);
-			}
-		}
-	}
-}

+ 0 - 437
oldcss/sass/screen.scss

@@ -1,437 +0,0 @@
-@import "_base";
-@import "compass/reset";
-
-@include establish-baseline;
-
-div, section, nav, article, header, footer {
-
-}
-
-body {
-	background: black;
-	@include text-size-adjust(none);
-	
-	font-family: "Open Sans", sans-serif;
-}
-
-a {
-	text-decoration: none;
-	color: $link-color;
-}
-
-strong {
-	font-weight: bold;
-}
-
-em {
-	font-style: italic;
-}
-
-@import "cookie";
-
-#top_banner {
-	background-repeat: no-repeat ! important;
-	background-position: center center ! important;
-	@include background-size(cover !important);
-	height: 120px;
-	background: black;
-	
-	@include breakpoint(max-width $bp-small) {
-		height: 80px;
-	}
-}
-
-#bold_title {
-	color: white;
-	@include container;
-	position: relative;
-	font-family: 'Oswald', sans-serif;
-	font-weight: normal;
-	@include single-text-shadow(black, 0px, 0px, 3px);
-	
-	h1 {
-		font-size: 3em;
-		text-align: right;
-		position: relative;
-		top: 0.75em;
-		@include span(3 first);
-		@include nowrap();
-	}
-	
-	.tagline {
-		font-size: 1.5em;
-		text-align: left;
-		@include span(3 last);
-		
-		span {
-			position: absolute;
-			margin-left: -1*span(2);
-			top: 3em;
-		}
-	}
-	
-	@include breakpoint(max-width $bp-small) {
-		h1 {
-			font-size: 2em;
-			top: 0.5em;
-			@include span(5 split);
-		}
-		
-		.tagline span {
-			font-size: 0.75em;
-			top: 2.5em;
-		}
-	}
-}
-
-.page {
-	#content_box {
-		#inner_box {
-			@include container;
-			
-			article {
-				@include span(full);
-				padding-top: 2*$base-line-height;
-				
-				&.bottom_padding {
-					padding-bottom: 2*$base-line-height;
-				}
-
-				@include breakpoint(max-width $bp-middle) {
-					@include span(full split);
-				}
-				
-				@include breakpoint(max-width $bp-small) {
-					padding-top: $base-line-height;
-				}
-			
-				header {
-					text-align: center;
-				
-					h2 {
-						font-size: 2em;
-						font-family: "Oswald", sans-serif;
-						line-height: 2*$base-line-height;
-						padding-bottom: $base-line-height;
-					
-						@include breakpoint(max-width $bp-small) {
-							font-size: 1.5em;
-							padding-bottom: 0;
-						}
-					}
-				}
-				
-				h2 {
-					font-family: "Oswald", sans-serif;
-					font-size: 1.5em;
-					line-height: 2*$base-line-height;
-				}
-				
-				h3 {
-					@extend h2;
-					font-size: 1.25em;
-					
-					strong {
-						text-transform: uppercase;
-					}
-				}
-				
-				ul {
-					padding-left: 1em;
-					margin: $base-line-height/2 0;
-					
-					&.nomargin {
-						margin: 0;
-					}
-					
-					li {
-						text-indent: -0.7em;
-					}
-					
-					li:before {
-						content: '• ';
-						width: 1em;
-					}
-					
-					&.nobullets {
-						padding-left: 0;
-
-						li {
-							text-indent: 0;
-						}
-					
-						li:before {
-							content: '';
-							width: 0;
-						}
-					}
-				}
-				
-				p.calltoaction {
-					text-align: center;
-					display: block;
-					margin: $base-line-height/2 0;
-					padding: $base-line-height/2 0;
-					background-color: hsl(210, 88, 95);
-					font-size: 1.5em;
-					font-family: "Oswald", sans-serif;
-					
-					a {
-
-					}
-				}
-
-				section {
-					&.full {
-						@include span(full);
-						
-						&.hr {
-							height: 1px;
-							padding: 0;
-							margin: $base-line-height*0.73 0 $base-line-height*0.25 0;
-							background: hsl(0,0,80);
-						}
-					}
-					&.left {
-						@include span(first 3);
-						
-						@include breakpoint(max-width $bp-small) {
-							@include span(full);
-						}
-					}
-					&.right {
-						@include span(last 3);
-
-						@include breakpoint(max-width $bp-small) {
-							@include span(full);
-						}
-					}
-					&.focus {
-					}
-
-					blockquote {
-						padding: gutter() $base-line-height;
-						font-style: italic;
-
-						i, em {
-							font-style: normal;
-						}
-					}
-				}
-				
-				figure {
-					position: relative;
-					margin-top: $base-line-height/2;
-					margin-bottom: $base-line-height/2;
-		
-					img {
-						@include border-radius($corner-radius);
-						width: 100%;
-						display: block;
-					}
-
-					figcaption {
-						position: absolute;
-						width: 100%;
-						bottom: 0;
-						font-size: 0.75em;
-						color: #fff;
-						line-height: $base-line-height;
-						background: rgba(0, 0, 0, 0.5);
-						padding: 0 0 0.2em 0;
-						@include border-radius(0 0 $corner-radius $corner-radius);
-			
-						a {
-							color: #fff;
-							text-decoration: underline;
-						}
-			
-						span {
-							padding: 0 1em;
-				
-							&:before {
-								content: '\203A  ';
-							}
-						}
-					}
-				}
-			}
-		}
-	}
-}
-
-.home.page {
-	#bold_title {
-		h1 {
-			text-align: center;
-			@include span(6);
-			
-			.hi {
-				font-size: 0.667em;
-				vertical-align: super;
-				margin-right: 0.25em;
-			}
-		}
-		
-		@include breakpoint(max-width $bp-small) {
-			h1 {
-				font-size: 3em;
-				top: 0.35em;
-			}
-		
-			.tagline span {
-				background: red;
-				font-size: 0.75em;
-			}
-		}
-	}
-}
-
-#content_box {
-	background: white;
-	position: relative;
-	
-	#main_menu {
-		background: black;
-		@include container;
-		overflow: hidden;
-				
-		ul {
-			position: absolute;
-			bottom: -42px;
-			text-align: center;
-			@include nowrap();
-			overflow-x: auto;
-			overflow-y: hidden;
-			-webkit-overflow-scrolling: touch;
-
-			li {
-				padding: 10px 15px 5px 0;
-				display: inline-block;
-			}
-
-			a {
-				color: white;
-				font-family: "Oswald", sans-serif;
-				font-weight: normal;
-			}
-
-			@include breakpoint(max-width $bp-middle) {
-				@include span(full split);
-				background: url('../images/hscroller-01.png') bottom center no-repeat;
-				background-size: auto 100%;
-			}
-		}
-	}
-}
-
-#inner_box {
-	@include container;
-}
-
-.home.page {
-	#inner_box {
-		padding-bottom: 0;
-	}
-	
-	h2 {
-		line-height: 80px;
-		text-align: center;
-		font-size: 2em;
-		font-family: 'Oswald', sans-serif;
-	}
-	
-	.work.column {
-		width: 25%;
-		height: 300px;
-		float: left;
-		display: block;
-		padding: 0;
-		margin: 0;
-		position: relative;
-		vertical-align:bottom;
-		background: gray;		
-		
-		span {
-			position: absolute;
-			color: white;
-			font-family: 'Oswald', sans-serif;
-			font-size: 30px;
-			white-space: nowrap;
-			@include apply-origin(bottom left, false);
-			@include rotate(-90deg);
-			display: block;
-			bottom: 0.33em;
-			left: em(16px) + 0.33em;
-			overflow: visible;
-		}
-		
-		&.research {
-			background-image: url("../images/home-research-2x.png");
-			background-position: right bottom;
-			background-size: cover;
-		}
-		&.development {
-			background-image: url("../images/home-development-2x.png");
-			background-position: right bottom;
-			background-size: cover;
-		}
-		&.writing {
-			background-image: url("../images/home-writing-2x.png");
-			background-position: right bottom;
-			background-size: cover;
-		}
-		&.other {
-			background-image: url("../images/home-contact-2x.png");
-			background-position: right bottom;
-			background-size: cover;
-		}
-	}
-}
-
-#site_footer {
-	@include container;
-	color: adjust-lightness($link-color, 40);
-	line-height: 0.8*$base-line-height;
-
-	.footer_body {
-		padding-top: 46px;
-		border-bottom: 1px solid hsl(210, 25, 50);
-		
-		a {
-			color: adjust-lightness($link-color, 20);
-		}
-
-		.column {
-			padding-top: 14px;
-			font-size: 0.75em;
-			
-			&.first {
-				@include span(first 1);
-			}
-			&.second {
-				@include span(1);
-			}
-			
-			&.last {
-				@include span(last 4);
-			}
-			
-			@include breakpoint(max-width $bp-middle) {
-				&.first {
-					@include span(2);
-					@include pre(gutter()/2);
-				}
-			
-				&.last {
-					@include span(last 3);
-					@include post(gutter()/2);
-				}				
-			}
-		}
-	}
-}
-
-@import "_contact";
-@import "_development";
-@import "_cv";

+ 0 - 825
oldcss/screen.css

@@ -1,825 +0,0 @@
-@charset "UTF-8";
-/* line 5, ../../../.rvm/gems/ruby-2.6.4/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
-html, body, div, span, applet, object, iframe,
-h1, h2, .page #content_box #inner_box article h3, h3, h4, h5, h6, p, blockquote, pre,
-a, abbr, acronym, address, big, cite, code,
-del, dfn, em, img, ins, kbd, q, s, samp,
-small, strike, strong, sub, sup, tt, var,
-b, u, i, center,
-dl, dt, dd, ol, ul, li,
-fieldset, form, label, legend,
-table, caption, tbody, tfoot, thead, tr, th, td,
-article, aside, canvas, details, embed,
-figure, figcaption, footer, header, hgroup,
-menu, nav, output, ruby, section, summary,
-time, mark, audio, video {
-  margin: 0;
-  padding: 0;
-  border: 0;
-  font: inherit;
-  font-size: 100%;
-  vertical-align: baseline;
-}
-
-/* line 22, ../../../.rvm/gems/ruby-2.6.4/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
-html {
-  line-height: 1;
-}
-
-/* line 24, ../../../.rvm/gems/ruby-2.6.4/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
-ol, ul {
-  list-style: none;
-}
-
-/* line 26, ../../../.rvm/gems/ruby-2.6.4/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
-table {
-  border-collapse: collapse;
-  border-spacing: 0;
-}
-
-/* line 28, ../../../.rvm/gems/ruby-2.6.4/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
-caption, th, td {
-  text-align: left;
-  font-weight: normal;
-  vertical-align: middle;
-}
-
-/* line 30, ../../../.rvm/gems/ruby-2.6.4/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
-q, blockquote {
-  quotes: none;
-}
-/* line 103, ../../../.rvm/gems/ruby-2.6.4/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
-q:before, q:after, blockquote:before, blockquote:after {
-  content: "";
-  content: none;
-}
-
-/* line 32, ../../../.rvm/gems/ruby-2.6.4/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
-a img {
-  border: none;
-}
-
-/* line 116, ../../../.rvm/gems/ruby-2.6.4/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
-article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary {
-  display: block;
-}
-
-/* line 106, ../../../.rvm/gems/ruby-2.6.4/gems/compass-core-1.0.3/stylesheets/compass/typography/_vertical_rhythm.scss */
-html {
-  font-size: 100%;
-  line-height: 1.75em;
-}
-
-/* line 10, sass/screen.scss */
-body {
-  background: black;
-  -webkit-text-size-adjust: none;
-  -moz-text-size-adjust: none;
-  -ms-text-size-adjust: none;
-  font-family: "Open Sans", sans-serif;
-}
-
-/* line 17, sass/screen.scss */
-a {
-  text-decoration: none;
-  color: #4080bf;
-}
-
-/* line 22, sass/screen.scss */
-strong {
-  font-weight: bold;
-}
-
-/* line 26, sass/screen.scss */
-em {
-  font-style: italic;
-}
-
-/* line 1, sass/cookie.scss */
-#cookie_monster {
-  position: fixed;
-  left: 0;
-  width: 100%;
-  background: rgba(255, 255, 255, 0.8);
-  font-family: 'Open Sans', sans-serif;
-  z-index: 255;
-}
-@media (min-width: 48em) {
-  /* line 1, sass/cookie.scss */
-  #cookie_monster {
-    top: 0;
-  }
-}
-@media (max-width: 48em) {
-  /* line 1, sass/cookie.scss */
-  #cookie_monster {
-    bottom: 0;
-    background: rgba(239, 239, 143, 0.8);
-  }
-}
-/* line 18, sass/cookie.scss */
-#cookie_monster .content {
-  max-width: 48em;
-  margin-left: auto;
-  margin-right: auto;
-  margin: 0 auto;
-  color: black;
-  padding: 0.375em 1.5em 0.1875em 1.5em;
-}
-/* line 12, ../../../.rvm/gems/ruby-2.6.4/gems/susy-2.2.14/sass/susy/output/support/_clearfix.scss */
-#cookie_monster .content:after {
-  content: " ";
-  display: block;
-  clear: both;
-}
-/* line 24, sass/cookie.scss */
-#cookie_monster .content strong {
-  font-weight: bold;
-}
-/* line 28, sass/cookie.scss */
-#cookie_monster .content .button {
-  display: block;
-  background: black;
-  color: white;
-  padding: 0.375em 0.5625em;
-  margin-top: 0.375em;
-  margin-left: 0.1875em;
-  float: right;
-  -moz-border-radius: 4px;
-  -webkit-border-radius: 4px;
-  border-radius: 4px;
-  border: 0;
-  font-size: 16px;
-}
-/* line 40, sass/cookie.scss */
-#cookie_monster .content .button.no {
-  background: #34b273;
-}
-/* line 44, sass/cookie.scss */
-#cookie_monster .content .button.yes {
-  font-weight: bold;
-  background: #c0598c;
-}
-
-/* line 32, sass/screen.scss */
-#top_banner {
-  background-repeat: no-repeat !important;
-  background-position: center center !important;
-  -moz-background-size: cover !important;
-  -o-background-size: cover !important;
-  -webkit-background-size: cover !important;
-  background-size: cover !important;
-  height: 120px;
-  background: black;
-}
-@media (max-width: 32em) {
-  /* line 32, sass/screen.scss */
-  #top_banner {
-    height: 80px;
-  }
-}
-
-/* line 44, sass/screen.scss */
-#bold_title {
-  color: white;
-  max-width: 48em;
-  margin-left: auto;
-  margin-right: auto;
-  position: relative;
-  font-family: 'Oswald', sans-serif;
-  font-weight: normal;
-  text-shadow: 0px 0px 3px black;
-}
-/* line 12, ../../../.rvm/gems/ruby-2.6.4/gems/susy-2.2.14/sass/susy/output/support/_clearfix.scss */
-#bold_title:after {
-  content: " ";
-  display: block;
-  clear: both;
-}
-/* line 52, sass/screen.scss */
-#bold_title h1 {
-  font-size: 3em;
-  text-align: right;
-  position: relative;
-  top: 0.75em;
-  width: 47.82609%;
-  float: left;
-  margin-right: 4.34783%;
-  white-space: nowrap;
-}
-/* line 61, sass/screen.scss */
-#bold_title .tagline {
-  font-size: 1.5em;
-  text-align: left;
-  width: 47.82609%;
-  float: right;
-  margin-right: 0;
-}
-/* line 66, sass/screen.scss */
-#bold_title .tagline span {
-  position: absolute;
-  margin-left: -30.43478%;
-  top: 3em;
-}
-@media (max-width: 32em) {
-  /* line 74, sass/screen.scss */
-  #bold_title h1 {
-    font-size: 2em;
-    top: 0.5em;
-    width: 79.16667%;
-    float: left;
-    margin-left: 2.08333%;
-    margin-right: 2.08333%;
-  }
-  /* line 80, sass/screen.scss */
-  #bold_title .tagline span {
-    font-size: 0.75em;
-    top: 2.5em;
-  }
-}
-
-/* line 89, sass/screen.scss */
-.page #content_box #inner_box {
-  max-width: 48em;
-  margin-left: auto;
-  margin-right: auto;
-}
-/* line 12, ../../../.rvm/gems/ruby-2.6.4/gems/susy-2.2.14/sass/susy/output/support/_clearfix.scss */
-.page #content_box #inner_box:after {
-  content: " ";
-  display: block;
-  clear: both;
-}
-/* line 92, sass/screen.scss */
-.page #content_box #inner_box article {
-  width: 100%;
-  float: left;
-  margin-left: 0;
-  margin-right: 0;
-  padding-top: 56px;
-}
-/* line 96, sass/screen.scss */
-.page #content_box #inner_box article.bottom_padding {
-  padding-bottom: 56px;
-}
-@media (max-width: 48em) {
-  /* line 92, sass/screen.scss */
-  .page #content_box #inner_box article {
-    width: 95.83333%;
-    float: left;
-    margin-left: 2.08333%;
-    margin-right: 2.08333%;
-  }
-}
-@media (max-width: 32em) {
-  /* line 92, sass/screen.scss */
-  .page #content_box #inner_box article {
-    padding-top: 28px;
-  }
-}
-/* line 108, sass/screen.scss */
-.page #content_box #inner_box article header {
-  text-align: center;
-}
-/* line 111, sass/screen.scss */
-.page #content_box #inner_box article header h2, .page #content_box #inner_box article header h3 {
-  font-size: 2em;
-  font-family: "Oswald", sans-serif;
-  line-height: 56px;
-  padding-bottom: 28px;
-}
-@media (max-width: 32em) {
-  /* line 111, sass/screen.scss */
-  .page #content_box #inner_box article header h2, .page #content_box #inner_box article header h3 {
-    font-size: 1.5em;
-    padding-bottom: 0;
-  }
-}
-/* line 124, sass/screen.scss */
-.page #content_box #inner_box article h2, .page #content_box #inner_box article h3 {
-  font-family: "Oswald", sans-serif;
-  font-size: 1.5em;
-  line-height: 56px;
-}
-/* line 130, sass/screen.scss */
-.page #content_box #inner_box article h3 {
-  font-size: 1.25em;
-}
-/* line 134, sass/screen.scss */
-.page #content_box #inner_box article h3 strong {
-  text-transform: uppercase;
-}
-/* line 139, sass/screen.scss */
-.page #content_box #inner_box article ul {
-  padding-left: 1em;
-  margin: 14px 0;
-}
-/* line 143, sass/screen.scss */
-.page #content_box #inner_box article ul.nomargin {
-  margin: 0;
-}
-/* line 147, sass/screen.scss */
-.page #content_box #inner_box article ul li {
-  text-indent: -0.7em;
-}
-/* line 151, sass/screen.scss */
-.page #content_box #inner_box article ul li:before {
-  content: '• ';
-  width: 1em;
-}
-/* line 156, sass/screen.scss */
-.page #content_box #inner_box article ul.nobullets {
-  padding-left: 0;
-}
-/* line 159, sass/screen.scss */
-.page #content_box #inner_box article ul.nobullets li {
-  text-indent: 0;
-}
-/* line 163, sass/screen.scss */
-.page #content_box #inner_box article ul.nobullets li:before {
-  content: '';
-  width: 0;
-}
-/* line 170, sass/screen.scss */
-.page #content_box #inner_box article p.calltoaction {
-  text-align: center;
-  display: block;
-  margin: 14px 0;
-  padding: 14px 0;
-  background-color: #e7f2fd;
-  font-size: 1.5em;
-  font-family: "Oswald", sans-serif;
-}
-/* line 185, sass/screen.scss */
-.page #content_box #inner_box article section.full {
-  width: 100%;
-  float: left;
-  margin-left: 0;
-  margin-right: 0;
-}
-/* line 188, sass/screen.scss */
-.page #content_box #inner_box article section.full.hr {
-  height: 1px;
-  padding: 0;
-  margin: 20.44px 0 7px 0;
-  background: #cccccc;
-}
-/* line 195, sass/screen.scss */
-.page #content_box #inner_box article section.left {
-  width: 47.82609%;
-  float: left;
-  margin-right: 4.34783%;
-}
-@media (max-width: 32em) {
-  /* line 195, sass/screen.scss */
-  .page #content_box #inner_box article section.left {
-    width: 100%;
-    float: left;
-    margin-left: 0;
-    margin-right: 0;
-  }
-}
-/* line 202, sass/screen.scss */
-.page #content_box #inner_box article section.right {
-  width: 47.82609%;
-  float: right;
-  margin-right: 0;
-}
-@media (max-width: 32em) {
-  /* line 202, sass/screen.scss */
-  .page #content_box #inner_box article section.right {
-    width: 100%;
-    float: left;
-    margin-left: 0;
-    margin-right: 0;
-  }
-}
-/* line 212, sass/screen.scss */
-.page #content_box #inner_box article section blockquote {
-  padding: 4.34783% 28px;
-  font-style: italic;
-}
-/* line 216, sass/screen.scss */
-.page #content_box #inner_box article section blockquote i, .page #content_box #inner_box article section blockquote em {
-  font-style: normal;
-}
-/* line 222, sass/screen.scss */
-.page #content_box #inner_box article figure {
-  position: relative;
-  margin-top: 14px;
-  margin-bottom: 14px;
-}
-/* line 227, sass/screen.scss */
-.page #content_box #inner_box article figure img {
-  -moz-border-radius: 4px;
-  -webkit-border-radius: 4px;
-  border-radius: 4px;
-  width: 100%;
-  display: block;
-}
-/* line 233, sass/screen.scss */
-.page #content_box #inner_box article figure figcaption {
-  position: absolute;
-  width: 100%;
-  bottom: 0;
-  font-size: 0.75em;
-  color: #fff;
-  line-height: 28px;
-  background: rgba(0, 0, 0, 0.5);
-  padding: 0 0 0.2em 0;
-  -moz-border-radius: 0 0 4px 4px;
-  -webkit-border-radius: 0;
-  border-radius: 0 0 4px 4px;
-}
-/* line 244, sass/screen.scss */
-.page #content_box #inner_box article figure figcaption a {
-  color: #fff;
-  text-decoration: underline;
-}
-/* line 249, sass/screen.scss */
-.page #content_box #inner_box article figure figcaption span {
-  padding: 0 1em;
-}
-/* line 252, sass/screen.scss */
-.page #content_box #inner_box article figure figcaption span:before {
-  content: '\203A  ';
-}
-
-/* line 265, sass/screen.scss */
-.home.page #bold_title h1 {
-  text-align: center;
-  width: 100%;
-  float: left;
-  margin-left: 0;
-  margin-right: 0;
-}
-/* line 269, sass/screen.scss */
-.home.page #bold_title h1 .hi {
-  font-size: 0.667em;
-  vertical-align: super;
-  margin-right: 0.25em;
-}
-@media (max-width: 32em) {
-  /* line 277, sass/screen.scss */
-  .home.page #bold_title h1 {
-    font-size: 3em;
-    top: 0.35em;
-  }
-  /* line 282, sass/screen.scss */
-  .home.page #bold_title .tagline span {
-    background: red;
-    font-size: 0.75em;
-  }
-}
-
-/* line 290, sass/screen.scss */
-#content_box {
-  background: white;
-  position: relative;
-}
-/* line 294, sass/screen.scss */
-#content_box #main_menu {
-  background: black;
-  max-width: 48em;
-  margin-left: auto;
-  margin-right: auto;
-  overflow: hidden;
-}
-/* line 12, ../../../.rvm/gems/ruby-2.6.4/gems/susy-2.2.14/sass/susy/output/support/_clearfix.scss */
-#content_box #main_menu:after {
-  content: " ";
-  display: block;
-  clear: both;
-}
-/* line 299, sass/screen.scss */
-#content_box #main_menu ul {
-  position: absolute;
-  bottom: -42px;
-  text-align: center;
-  white-space: nowrap;
-  overflow-x: auto;
-  overflow-y: hidden;
-  -webkit-overflow-scrolling: touch;
-}
-/* line 308, sass/screen.scss */
-#content_box #main_menu ul li {
-  padding: 10px 15px 5px 0;
-  display: inline-block;
-}
-/* line 313, sass/screen.scss */
-#content_box #main_menu ul a {
-  color: white;
-  font-family: "Oswald", sans-serif;
-  font-weight: normal;
-}
-@media (max-width: 48em) {
-  /* line 299, sass/screen.scss */
-  #content_box #main_menu ul {
-    width: 95.83333%;
-    float: left;
-    margin-left: 2.08333%;
-    margin-right: 2.08333%;
-    background: url("../images/hscroller-01.png") bottom center no-repeat;
-    background-size: auto 100%;
-  }
-}
-
-/* line 328, sass/screen.scss */
-#inner_box {
-  max-width: 48em;
-  margin-left: auto;
-  margin-right: auto;
-}
-/* line 12, ../../../.rvm/gems/ruby-2.6.4/gems/susy-2.2.14/sass/susy/output/support/_clearfix.scss */
-#inner_box:after {
-  content: " ";
-  display: block;
-  clear: both;
-}
-
-/* line 333, sass/screen.scss */
-.home.page #inner_box {
-  padding-bottom: 0;
-}
-/* line 337, sass/screen.scss */
-.home.page h2, .home.page #content_box #inner_box article h3 {
-  line-height: 80px;
-  text-align: center;
-  font-size: 2em;
-  font-family: 'Oswald', sans-serif;
-}
-/* line 344, sass/screen.scss */
-.home.page .work.column {
-  width: 25%;
-  height: 300px;
-  float: left;
-  display: block;
-  padding: 0;
-  margin: 0;
-  position: relative;
-  vertical-align: bottom;
-  background: gray;
-}
-/* line 355, sass/screen.scss */
-.home.page .work.column span {
-  position: absolute;
-  color: white;
-  font-family: 'Oswald', sans-serif;
-  font-size: 30px;
-  white-space: nowrap;
-  -moz-transform-origin: bottom left;
-  -ms-transform-origin: bottom left;
-  -webkit-transform-origin: bottom left;
-  transform-origin: bottom left;
-  -moz-transform: rotate(-90deg);
-  -ms-transform: rotate(-90deg);
-  -webkit-transform: rotate(-90deg);
-  transform: rotate(-90deg);
-  display: block;
-  bottom: 0.33em;
-  left: 1.33em;
-  overflow: visible;
-}
-/* line 369, sass/screen.scss */
-.home.page .work.column.research {
-  background-image: url("../images/home-research-2x.png");
-  background-position: right bottom;
-  background-size: cover;
-}
-/* line 374, sass/screen.scss */
-.home.page .work.column.development {
-  background-image: url("../images/home-development-2x.png");
-  background-position: right bottom;
-  background-size: cover;
-}
-/* line 379, sass/screen.scss */
-.home.page .work.column.writing {
-  background-image: url("../images/home-writing-2x.png");
-  background-position: right bottom;
-  background-size: cover;
-}
-/* line 384, sass/screen.scss */
-.home.page .work.column.other {
-  background-image: url("../images/home-contact-2x.png");
-  background-position: right bottom;
-  background-size: cover;
-}
-
-/* line 392, sass/screen.scss */
-#site_footer {
-  max-width: 48em;
-  margin-left: auto;
-  margin-right: auto;
-  color: #d9e6f2;
-  line-height: 22.4px;
-}
-/* line 12, ../../../.rvm/gems/ruby-2.6.4/gems/susy-2.2.14/sass/susy/output/support/_clearfix.scss */
-#site_footer:after {
-  content: " ";
-  display: block;
-  clear: both;
-}
-/* line 397, sass/screen.scss */
-#site_footer .footer_body {
-  padding-top: 46px;
-  border-bottom: 1px solid #60809f;
-}
-/* line 401, sass/screen.scss */
-#site_footer .footer_body a {
-  color: #8cb3d9;
-}
-/* line 405, sass/screen.scss */
-#site_footer .footer_body .column {
-  padding-top: 14px;
-  font-size: 0.75em;
-}
-/* line 409, sass/screen.scss */
-#site_footer .footer_body .column.first {
-  width: 13.04348%;
-  float: left;
-  margin-right: 4.34783%;
-}
-/* line 412, sass/screen.scss */
-#site_footer .footer_body .column.second {
-  width: 13.04348%;
-  float: left;
-  margin-right: 4.34783%;
-}
-/* line 416, sass/screen.scss */
-#site_footer .footer_body .column.last {
-  width: 65.21739%;
-  float: right;
-  margin-right: 0;
-}
-@media (max-width: 48em) {
-  /* line 421, sass/screen.scss */
-  #site_footer .footer_body .column.first {
-    width: 30.43478%;
-    float: left;
-    margin-right: 4.34783%;
-    margin-left: 2.17391%;
-  }
-  /* line 426, sass/screen.scss */
-  #site_footer .footer_body .column.last {
-    width: 47.82609%;
-    float: right;
-    margin-right: 0;
-    margin-right: 2.17391%;
-  }
-}
-
-/* line 4, sass/_contact.scss */
-.contact.page #content_box article section.links {
-  width: 47.82609%;
-  float: left;
-  margin-right: 4.34783%;
-}
-@media (max-width: 48em) {
-  /* line 4, sass/_contact.scss */
-  .contact.page #content_box article section.links {
-    width: 100%;
-    float: left;
-    margin-left: 0;
-    margin-right: 0;
-  }
-}
-/* line 12, sass/_contact.scss */
-.contact.page #content_box article section.form {
-  width: 47.82609%;
-  float: right;
-  margin-right: 0;
-}
-@media (max-width: 48em) {
-  /* line 12, sass/_contact.scss */
-  .contact.page #content_box article section.form {
-    width: 100%;
-    float: left;
-    margin-left: 0;
-    margin-right: 0;
-  }
-}
-/* line 19, sass/_contact.scss */
-.contact.page #content_box article section.form p.contact_thanks {
-  background: #ecf9ec;
-  color: #0d730d;
-  padding: 0.5em 14px;
-  -moz-border-radius: 4px;
-  -webkit-border-radius: 4px;
-  border-radius: 4px;
-}
-/* line 26, sass/_contact.scss */
-.contact.page #content_box article section.form p.meanwhile {
-  padding: 0.5em 14px;
-}
-/* line 30, sass/_contact.scss */
-.contact.page #content_box article section.form .field {
-  display: block;
-  width: 100%;
-  padding-bottom: 28px;
-}
-/* line 35, sass/_contact.scss */
-.contact.page #content_box article section.form .field label {
-  display: block;
-  color: #9fbfdf;
-}
-/* line 40, sass/_contact.scss */
-.contact.page #content_box article section.form .field input, .contact.page #content_box article section.form .field textarea {
-  -moz-border-radius: 4px;
-  -webkit-border-radius: 4px;
-  border-radius: 4px;
-  padding: 0 0 0 0.25em;
-  display: block;
-  width: 100%;
-  height: 28px;
-  font-size: 1em;
-  font-family: "Open Sans", sans-serif;
-  border: 0;
-  border-bottom: 2px solid #4080bf;
-  background: #f7fafc;
-}
-@media (max-width: 48em) {
-  /* line 40, sass/_contact.scss */
-  .contact.page #content_box article section.form .field input, .contact.page #content_box article section.form .field textarea {
-    width: 95.83333%;
-  }
-}
-/* line 57, sass/_contact.scss */
-.contact.page #content_box article section.form .field textarea {
-  min-height: 140px;
-  max-width: 100%;
-}
-/* line 62, sass/_contact.scss */
-.contact.page #content_box article section.form .field input:focus, .contact.page #content_box article section.form .field textarea:focus {
-  outline: none;
-}
-/* line 66, sass/_contact.scss */
-.contact.page #content_box article section.form .field input.invalid, .contact.page #content_box article section.form .field textarea.invalid {
-  border-bottom: 2px solid #4080bf;
-  background: #f7fafc;
-}
-/* line 71, sass/_contact.scss */
-.contact.page #content_box article section.form .field input.valid, .contact.page #content_box article section.form .field textarea.valid {
-  border-bottom: 2px solid #4080bf;
-  background: white;
-}
-/* line 77, sass/_contact.scss */
-.contact.page #content_box article section.form .button {
-  text-align: right;
-}
-/* line 80, sass/_contact.scss */
-.contact.page #content_box article section.form .button button {
-  background: #3973ac;
-  font-weight: bold;
-  -moz-border-radius: 4px;
-  -webkit-border-radius: 4px;
-  border-radius: 4px;
-  color: white;
-  border: 0;
-  font-size: 1em;
-  line-height: 2.5em;
-  padding: 0 0.75em 0.1em 0.5em;
-}
-/* line 90, sass/_contact.scss */
-.contact.page #content_box article section.form .button button .fa {
-  padding-right: 0.25em;
-}
-/* line 94, sass/_contact.scss */
-.contact.page #content_box article section.form .button button:disabled {
-  background: #b3cce6;
-}
-
-@media (max-width: 32em) {
-  /* line 4, sass/_development.scss */
-  .development.page #content_box article section.project {
-    margin-top: 14px;
-    border-top: 1px solid #cccccc;
-    padding-top: 14px;
-  }
-  /* line 10, sass/_development.scss */
-  .development.page #content_box article section.project h3 {
-    line-height: 28px !important;
-  }
-}
-
-/* line 2, sass/_cv.scss */
-.page.cv .macro {
-  margin-top: 28px;
-}
-/* line 5, sass/_cv.scss */
-.page.cv .macro.first {
-  margin-top: 0;
-}
-/* line 10, sass/_cv.scss */
-.page.cv .macro .body section {
-  margin-top: 28px;
-}
-/* line 13, sass/_cv.scss */
-.page.cv .macro .body section:first-child, .page.cv .macro .body section#full_cv {
-  margin-top: 0;
-}
-/* line 17, sass/_cv.scss */
-.page.cv .macro .body section section {
-  margin-top: 0;
-  padding-left: 1em;
-  border-left: 3px solid #d9e6f2;
-}

+ 32 - 22
package.json

@@ -1,32 +1,42 @@
 {
-  "name": "andreafranceschini.org",
-  "version": "2.0.0",
-  "description": "https://andreafranceschini.org",
-  "main": "index.js",
+  "private": true,
   "scripts": {
-    "dev": "npx concurrently \"npm run dev:css\"",
-    "dev:css": "postcss pcss/*.css -d css --watch --verbose",
-    "build": "NODE_ENV=production npm run build:css",
-    "build:css": "postcss pcss/*.css -d css --verbose"
+    "build": "nuxi build",
+    "dev": "nuxi dev",
+    "generate": "nuxi generate",
+    "preview": "nuxi preview",
+    "deploy": "nuxi build && rsync -avPz --delete -e ssh .output/* vps:/var/server/htdocs/andreafranceschini.org/www/ && ssh vps docker restart aforg"
   },
-  "repository": {
-    "type": "git",
-    "url": "ssh://git@git.morpheu5.net:2222/Morpheu5/af.git"
+  "dependencies": {
   },
-  "author": "Andrea Franceschini",
-  "license": "UNLICENSED",
   "devDependencies": {
-    "@tailwindcss/typography": "^0.5.2",
+    "@fortawesome/fontawesome-svg-core": "^6.1.2",
+    "@fortawesome/free-brands-svg-icons": "^6.1.2",
+    "@fortawesome/free-regular-svg-icons": "^6.1.2",
+    "@fortawesome/free-solid-svg-icons": "^6.1.2",
+    "@fortawesome/vue-fontawesome": "^3.0.1",
+    "@nuxtjs/google-fonts": "^3.0.0-0",
+    "@pinia/nuxt": "^0.4.0",
+    "@tailwindcss/aspect-ratio": "^0.4.0",
+    "@tailwindcss/typography": "^0.5.4",
+    "@types/animejs": "^3.1.4",
+    "@types/luxon": "^3.0.0",
+    "@types/three": "^0.143.1",
+    "animejs": "^3.2.1",
     "autoprefixer": "^10.4.7",
-    "concurrently": "^7.2.1",
-    "cssnano": "^5.1.11",
-    "postcss": "^8.0.9",
-    "postcss-cli": "^9.1.0",
-    "postcss-color-function": "^4.1.0",
+    "cssnano": "^5.1.12",
+    "daisyui": "^2.24.0",
+    "luxon": "^3.0.1",
+    "nuxt": "^3.0.0-rc.8",
+    "pinia-plugin-persistedstate": "^2.1.1",
+    "postcss": "^8.4.14",
     "postcss-import": "^14.1.0",
-    "postcss-mixins": "^9.0.2",
+    "postcss-mixins": "^9.0.3",
     "postcss-nested": "^5.0.6",
-    "postcss-preset-env": "^7.7.1",
-    "tailwindcss": "^3.0.24"
+    "postcss-preset-env": "^7.7.2",
+    "tailwindcss": "^3.1.3",
+    "three": "^0.143.0",
+    "typescript": "^4.7.4",
+    "vue-tsc": "^0.40.1"
   }
 }

+ 21 - 0
pages/404.vue

@@ -0,0 +1,21 @@
+<template><NuxtLayout name="default">
+    <template #headline>{{ headline }}</template>
+    <template #tagline>{{ tagline }}</template>
+
+    <div class="container mx-auto max-w-screen-lg sm:px-12 px-2 mb-12 prose">
+        <article class="body grid grid-cols-1 gap-x-12 gap-y-0">
+            <p>Things come, things go. The page you were looking for must have gone.</p>
+            <p>Sorry.</p>
+            <p>You can <a href="contact">let me know</a> and I'll do my best.</p>
+        </article>
+    </div>
+</NuxtLayout></template>
+
+<script setup>
+definePageMeta({
+    layout: false
+});
+
+const headline = "404";
+const tagline = "means I can't find what you're looking for.";
+</script>

+ 0 - 59
pages/_footer.php

@@ -1,59 +0,0 @@
-			<?php if($bodyclasses != 'home page'): ?>
-				<article class="pt-8 pb-4 print:hidden">
-					<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
-					<!-- af-footer -->
-					<ins class="adsbygoogle"
-						style="display:block"
-						data-ad-client="ca-pub-5066013786478491"
-						data-ad-slot="7289960963"
-						data-ad-format="auto"></ins>
-					<script>
-					(adsbygoogle = window.adsbygoogle || []).push({});
-					</script>
-					<p class="text-xs text-gray-600 text-center">Ads help me keep this site up :) If you see a blank space up here, please disable your ad blocker.</p>
-				</article>
-			<?php endif ?>
-			</div> <!-- #inner_box -->
-		</div> <!-- #main_flex -->
-	</div> <!-- #content_box -->
-
-	<footer id="site_footer" class="bg-black border-t border-gray-900 text-white text-xs text-darkerLinks print:hidden">
-		<div class="footer_body max-w-screen-md mx-auto px-4 sm:px-0 py-4 grid grid-cols-1 sm:grid-cols-6">
-			<div class="col-start-1 col-span-1">
-				<p><a href="<?=$baseurl?>colophon">About this site</a></p>
-			</div>
-			<div class="col-start-1 col-span-1 sm:col-start-2 sm:col-span-5">
-				<p class="mt-2 sm:mt-0 sm:text-right">Unless otherwise specified, the content of this site is licensed as <a href="//creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a>.</p>
-			</div>
-		</div>
-	</footer>
-
-	<!-- Global site tag (gtag.js) - Google Analytics -->
-	<!-- <script async src="https://www.googletagmanager.com/gtag/js?id=UA-1176762-5"></script>
-	<script>
-	window.dataLayer = window.dataLayer || [];
-	function gtag(){dataLayer.push(arguments);}
-	gtag('js', new Date());
-
-	gtag('config', 'UA-1176762-5');
-	</script> -->
-	
-	<!-- Matomo -->
-	<script>
-	  var _paq = window._paq = window._paq || [];
-	  /* tracker methods like "setCustomDimension" should be called before "trackPageView" */
-	  _paq.push(['trackPageView']);
-	  _paq.push(['enableLinkTracking']);
-	  (function() {
-	    var u="https://matomo.morpheu5.net/";
-	    _paq.push(['setTrackerUrl', u+'matomo.php']);
-	    _paq.push(['setSiteId', '1']);
-	    var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
-	    g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
-	  })();
-	</script>
-	<!-- End Matomo Code -->
-	
-</body>
-
-</html>

+ 0 - 11
pages/_ga.php

@@ -1,11 +0,0 @@
-<?php if(array_key_exists('eu-disable', $_COOKIE) and $_COOKIE["eu-disable"] == "false") : ?>
-    <!-- Global site tag (gtag.js) - Google Analytics -->
-    <script async src="https://www.googletagmanager.com/gtag/js?id=UA-1176762-5"></script>
-    <script>
-        window.dataLayer = window.dataLayer || [];
-        function gtag(){dataLayer.push(arguments);}
-        gtag('js', new Date());
-
-        gtag('config', 'UA-1176762-5');
-    </script>
-<?php endif ?>

+ 0 - 82
pages/_head.php

@@ -1,82 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-	<meta charset="utf-8">
-	<meta name="viewport" content="width=device-width, minimum-scale=1.0" />
-	
-<?php $_title = isset($title) ? $title . ' &mdash; Andrea Franceschini' : 'Andrea Franceschini' ?>
-	<title><?=$_title?></title>
-	<meta name="DC.Title" content="<?=$_title?>" />
-	<meta property="og:title" content="<?=$_title?>" />
-	<meta property="twitter:title" content="<?=$_title?>" />
-	
-<?php if(isset($keywords)) : ?>
-	<meta name="keywords" content="<?=$keywords?>" />
-<?php endif ?>
-	
-	<meta name="author" content="Andrea Franceschini" />
-	<meta name="DC.Creator" content="Andrea Franceschini" />
-	<!-- <meta name="DC.Contributor" content="Andrea Franceschini" /> -->
-	<meta name="twitter:site" content="@morpheu5" />
-	<meta name="twitter:creator" content="@morpheu5" />
-
-<?php if(isset($description)) : ?>
-	<meta name="description" content="<?=$description?>" />
-	<meta name="DC.Description" content="<?=$description?>" />
-	<meta property="og:description" content="<?=$description?>" />
-<?php endif ?>
-	<meta property="twitter:description" content="<?=isset($description) ? $description : 'PhD student at The Open University' ?>" />
-		
-	<!--   Twitter card extras -->
-	<meta name="twitter:card" content="summary_large_image" />
-<?php if(isset($twitter_image)) : ?>
-	<meta name="twitter:image:src" content="<?=$baseurl.$twitter_image?>" />
-<?php endif ?>
-	<meta name="twitter:domain" content="<?=$baseurl?>" />
-	<!-- / Twitter card extras -->
-
-	<!--   Open Graph madness -->
-	<meta property="og:type" content="website" />
-<?php if(isset($twitter_image)) : ?>
-	<meta property="og:image" content="<?=$baseurl.$twitter_image?>" />
-<?php endif ?>
-	<!-- / Open Graph madness -->
-	
-	<!--   Dublin Core extras -->
-	<meta name="DC.Rights" content="CC BY-NC-SA 4.0" />
-	<!-- / Dublin Core extras -->
-
-	<meta name="robots" content="All" />
-
-	<link rel="stylesheet" href="<?=$baseurl?>css/screen.css" type="text/css" media="screen" />
-	<link rel="stylesheet" href="<?=$baseurl?>css/print.css" type="text/css" media="print" />
-	<?php if(isset($css)) : ?>
-	<?php foreach($css as $file) : ?>
-	<link rel="stylesheet" href="<?=$baseurl?>css/<?=$file?>.css" type="text/css" media="screen" />
-	<?php endforeach ?>
-	<?php endif ?>
-	<link href='//fonts.googleapis.com/css?family=Oswald:400,700&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
-	<link href='//fonts.googleapis.com/css?family=Open+Sans:400,400italic,700,700italic&subset=latin,greek,greek-ext,latin-ext' rel='stylesheet' type='text/css'>
-	
-	<link href="//cdn.rawgit.com/noelboss/featherlight/1.7.1/release/featherlight.min.css" type="text/css" rel="stylesheet" />
-	
-	<!-- <link rel="schema.DC" href="http://purl.org/dc/elements/1.1/"> -->
-	
-	<!--[if lt IE 9]>
-	<script src="<?=$baseurl?>js/html5.min.js"></script>
-	<![endif]-->
-	<script type="text/javascript" src="<?=$baseurl?>js/js.cookie.js"></script>
- 	<script type="text/javascript" src="<?=$baseurl?>js/jquery.min.js"></script>
- 	<script type="text/javascript" src="<?=$baseurl?>js/picturefill.min.js"></script>
-	
-	<script src="//cdn.rawgit.com/noelboss/featherlight/1.7.1/release/featherlight.min.js" type="text/javascript" charset="utf-8"></script>	
-
-	<script src="https://kit.fontawesome.com/3960e26f79.js" crossorigin="anonymous"></script>
-
-	<script type="text/javascript" src="<?=$baseurl?>js/scripts.js"></script>
-	<?php if(isset($js)) : ?>
-	<?php foreach($js as $file) : ?>
-	<script type="text/javascript" src="<?=$baseurl?>js/<?=$file?>.js"></script>
-	<?php endforeach ?>
-	<?php endif ?>
-</head>

+ 0 - 25
pages/_header.php

@@ -1,25 +0,0 @@
-<?=i('_head')?>
-
-<body <?= isset($bodyclasses)? 'class="'.$bodyclasses.'"' : '' ?>>
-	<?=i('_ga')?>
-	<?php if(isset($banner_image) and isset($banner_image_half)) : ?>
-	<div id="top_banner" style="background-image: url('<?=$baseurl.$banner_image_half ?>')" data-hq="<?=$baseurl.$banner_image ?>">
-	<?php else : ?>
-	<div id="top_banner" style="background-image: url('<?=$baseurl ?>images/banner-half.jpg')" data-hq="<?=$baseurl ?>images/banner.jpg">
-	<?php endif ?>
-		<div id="bold_title" class="container mx-auto max-w-screen-md grid grid-6">
-		<?php if(stristr($bodyclasses, 'home')) : ?>
-			<h1>Hello</h1>
-			<p class="tagline"><span>I'm Andrea</span></p>
-		<?php else : ?>
-			<h1><?= isset($title) ? $title : '' ?></h1>
-			<p class="tagline print:hidden"><span><?= isset($tagline) ? $tagline : "" ?></span></p>
-		<?php endif ?>
-		</div>
-	</div>
-	
-	<div id="content_box" class="">
-		<div id="main_flex" class="flex flex-col">
-			<?=i('_main_menu')?>
-			
-			<div id="inner_box" class="container md:max-w-screen-md mx-auto prose max-w-none <?= stristr($bodyclasses, 'home') ? 'px-0' : 'px-4' ?> md:px-0 pt-6">

+ 0 - 12
pages/_main_menu.php

@@ -1,12 +0,0 @@
-		<nav id="main_menu" class="order-last bg-black print:hidden">
-			<div class="inner_box overflow-x-scroll overflow-y-hidden whitespace-no-wrap">
-				<ul class="flex md:px-0 pb-2 justify-start ti:justify-around mx-auto max-w-screen-md">
-					<li class="ml-4 md:ml-0"><a href="<?=url('')?>">Home</a></li>
-					<li><a href="<?=url('research')?>">Research</a></li>
-					<li><a href="<?=url('development')?>">Development</a></li>
-					<li><a href="<?=url('writing')?>">Writing</a></li>
-					<li><a href="<?=url('cv')?>">CV</a></li>
-					<li><a href="<?=url('contact')?>">Contact</a></li>
-				</ul>
-			</div>
-		</nav>

+ 0 - 28
pages/api/mailer.php

@@ -1,28 +0,0 @@
-<?php
-
-if($_SERVER['REQUEST_METHOD'] != 'POST') {
-	include error(403);
-	die;
-}
-
-if($_POST['captcha'] != '') {
-	include error(403);
-	die;
-}
-
-$senderName = trim(filter_var($_POST['name'], FILTER_SANITIZE_STRING));
-$senderEmail = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
-$message = filter_var($_POST['message'], FILTER_SANITIZE_STRING);
-
-$headers  = '';
-$headers .= "Content-Type: text/plain; charset=utf-8\r\n";
-$headers .= "From: $senderName <$senderEmail>\r\n";
-
-$result = mail("andrea.franceschini@gmail.com", "Website contact", $message, $headers);
-
-if($result == 1) {
-	$ref = filter_var($_SERVER['HTTP_REFERER'], FILTER_SANITIZE_URL);
-	session_start();
-	$_SESSION['contact_thanks'] = 1;
-	header("Location: $ref");
-}

+ 0 - 22
pages/cbprivacy.php

@@ -1,22 +0,0 @@
-<?php
-$bodyclasses = 'page';
-$title = 'Circular Bells privacy policy';
-$tagline = '';
-$description = 'Privacy policy for Circular Bells';
-$keywords = '';
-$twitter_image = '';
-?>
-
-<?=i('_header')?>
-
-	<article class="body grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-0">
-		<section class="full col-start-1 sm:col-span-2">
-			<h2>Circular Bells Privacy Policy</h2>
-			<p>Circular Bells does not collect personal data without your consent.</p>
-			<p>At the moment of writing (25 October 2016) Circular Bells does not collect any personal data at all, neither accesses any sensitive data on your device.</p>
-			<p><strong>This may change in the future, and you will be informed</strong></p>
-			<p>&nbsp;</p>
-		</section>
-	</article>
-
-<?=i('_footer')?>

+ 29 - 0
pages/cbprivacy.vue

@@ -0,0 +1,29 @@
+<template><NuxtLayout name="default">
+	<template #headline>{{ headline }}</template>
+	<template #tagline>{{ tagline }}</template>
+
+    <div class="container mx-auto max-w-screen-lg sm:px-12 px-2 prose">
+        <article class="body grid grid-cols-1 gap-x-12 gap-y-0">
+			<p>Circular Bells does not collect personal data without your consent.</p>
+			<p>At the moment of writing (25 August 2022) Circular Bells does not collect any personal data at all, neither accesses any sensitive data on your device.</p>
+			<p><strong>This may change in the future, and you will be informed</strong></p>
+			<p>&nbsp;</p>
+        </article>
+    </div>
+</NuxtLayout></template>
+
+<script lang="ts" setup>
+definePageMeta({
+    layout: false
+});
+
+const headline = "Circular Bells privacy policy";
+const tagline = "";
+
+const title = `${headline} ••• Andrea Franceschini, PhD`;
+
+useState('pageClasses', () => ['cbprivacy', 'page']);
+useHead({
+    title
+});
+</script>

+ 0 - 42
pages/colophon.php

@@ -1,42 +0,0 @@
-<?php
-$bodyclasses = 'colophon page';
-$title = 'About this site';
-$tagline = '';
-$description = 'Colophon, a funny little word historically meaning a statement at the end of a publication.';
-$keywords = 'colophon';
-$twitter_image = 'images/colopic.jpg';
-?>
-
-<?=i('_header')?>
-
-	<article class="body grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-0">
-		<h2>Colophon</h2>
-		<section class="col-start-1 col-span-1 sm:col-span-2">
-			<p class="noindent">Funny little word derived from Greek <span class="greek">κολοφων</span>, historically meaning a statement at the end of a publication, providing information about it. Think of metadata with a thick layer of dust on top.</p>
-		</section>
-		<section class="left col-start-1 col-span-1">
-			<h3>Author</h3>
-			<figure class="center">
-				<picture alt="This guy here.">
-					<source srcset="<?=$baseurl?>images/colopic.jpg 1x, <?=$baseurl?>images/colopic-2x.jpg 2x" />
-					<img class="rounded" srcset="<?=$baseurl ?>images/colopic.jpg" alt="This guy here." />
-				</picture>
-				<figcaption><span>This guy here, portrayed by <a href="//www.flickr.com/photos/xalira">Elena</a></span></figcaption>
-			</figure>
-			<p class="noindent">I am the sole author of this website's content, unless otherwise specified, and it's licensed as <a href="//creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a>.</p>
-		</section>
-		<section class="right col-start-1 col-span-1 sm:col-start-2">
-			<h3>Tech bits</h3>
-			<p class="noindent">For a whole bunch of reasons that I'm <strong>not</strong> about to explain, this website is lovingly crafted in plain <a href="//en.wikipedia.org/wiki/HTML5">HTML5</a>, with bits of JavaScript (<a href="//jquery.com">jQuery</a> flavoured), and a little <a href="//www.php.net">PHP</a> on the server side.
-				I'm using <a href="https://code.visualstudio.com/">Visual Studio Code</a> and sometimes <a href="//macromates.com/">TextMate 2</a> (now open source!) to edit the code.
-				The styling is provided by <a href="https://tailwindcss.com/">Compass</a> to make it look beautiful… and responsive!</p>
-			<h3>Fonts</h3>
-			<p class="noindent">I'm using a number of fonts:</p>
-			<ul>
-				<li>body and footer: <a href="//www.google.com/fonts/specimen/Open+Sans">Open Sans</a></li>
-				<li>headers: <a href="//www.google.com/fonts/specimen/Oswald">Oswald</a></li>
-			</ul>
-		</section>
-	</article>
-	
-<?=i('_footer')?>

+ 55 - 0
pages/colophon.vue

@@ -0,0 +1,55 @@
+<template><NuxtLayout name="default">
+    <template #headline>{{ headline }}</template>
+    <template #tagline>{{ tagline }}</template>
+
+    <div class="container mx-auto max-w-screen-lg sm:px-12 px-2 prose">
+        <article class="body grid sm:grid-cols-2 grid-cols-1 gap-x-12 gap-y-0">
+            <h2>Colophon</h2>
+            <section class="col-span-1 sm:col-span-2">
+                <p class="noindent">Funny little word derived from Greek <span class="greek">κολοφων</span>, historically meaning a statement at the end of a publication, providing information about it. Think of metadata with a thick layer of dust on top.</p>
+            </section>
+            <section>
+                <h3>Author</h3>
+                <figure class="center">
+                    <picture alt="This guy here.">
+                        <source srcset="/images/colopic.jpg 1x, /images/colopic-2x.jpg 2x" />
+                        <img class="rounded" srcset="/images/colopic.jpg" alt="This guy here." />
+                    </picture>
+                    <figcaption><span>This guy here, portrayed by <a href="https://www.flickr.com/photos/xalira">Elena</a></span></figcaption>
+                </figure>
+                <p class="noindent">I am the sole author of this website's content, unless otherwise specified, and it's licensed as <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a>.</p>
+            </section>
+            <section class="col-span-1 sm:col-start-2">
+                <h3>Tech bits</h3>
+                <p class="noindent">For a whole bunch of reasons that I'm <strong>not</strong> about to explain,
+                    this website is lovingly crafted in <a href="https://en.wikipedia.org/wiki/HTML5">HTML5</a>,
+                    with a little help from <a href="https://nuxtjs.org">NuxtJS</a> and <a href="https://threejs.org">Three.js</a>.
+                    I'm using <a href="https://code.visualstudio.com/">Visual Studio Code</a> to edit the code.
+                    The styling is provided by <a href="https://tailwindcss.com/">Tailwind CSS</a> to make it look beautiful… and responsive!</p>
+                <h3>Fonts</h3>
+                <p class="noindent">I'm using a number of fonts:</p>
+                <ul>
+                    <li>body and footer: <a href="https://www.google.com/fonts/specimen/Open+Sans">Open Sans</a></li>
+                    <li>headers: <a href="https://www.google.com/fonts/specimen/Oswald">Oswald</a></li>
+                    <li>icons: <a href="https://fontawesome.com">Font Awesome</a></li>
+                </ul>
+            </section>
+        </article>
+    </div>
+</NuxtLayout></template>
+
+<script lang="ts" setup>
+definePageMeta({
+    layout: false
+});
+
+const headline = "About this site";
+const tagline = "";
+
+const title = `${headline} ••• Andrea Franceschini, PhD`;
+
+useState('pageClasses', () => ['colophon', 'page']);
+useHead({
+    title
+});
+</script>

+ 0 - 69
pages/contact.php

@@ -1,69 +0,0 @@
-<?php
-$bodyclasses = 'contact page';
-$title = 'Contact me';
-$tagline = '';
-$description = 'Everything you need to know to get in touch with me.';
-$keywords = 'music, learning, tabletop, research, the open university';
-$twitter_image = 'images/colopic.jpg';
-
-$js = array('H5F', 'contact');
-
-session_start();
-?>
-
-<?=i('_header')?>
-
-	<article class="body grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-0">
-		<section class="links">
-			<p>You can find me on these web sites…</p>
-			<div class="text-center">
-				<a href="https://blog.morpheu5.net"><i class="fas fa-blog text-3xl mr-3" alt="MorphLog" style="color: black"></i></a>
-				<a href="https://orcid.org/0000-0001-8665-6147"><i class="fab fa-orcid text-3xl mr-3" alt="ORCID" style="color: #a6cf39"></i></a>
-				<a href="https://www.linkedin.com/in/andreafranceschini"><i class="fab fa-linkedin text-3xl mr-3" alt="LinkedIn" style="color: #0077b5"></i></a>
-				<a href="https://github.com/Morpheu5"><i class="fab fa-github text-3xl mr-3" alt="GitHub" style="color: black"></i></a>
-				<a href="https://apps.apple.com/it/developer/andrea-franceschini/id454062630"><i class="fab fa-app-store-ios text-3xl mr-3" alt="App Store" style="color: #2296f3"></i></a>
-				<a href="http://play.google.com/store/apps/developer?id=Andrea+Franceschini&hl=it_IT"><i class="fab fa-google-play text-3xl mr-3" alt="Google Play" style="color: #3BCCFF"></i></a>
-			</div>
-			<div class="mt-2 text-center">
-				<a href="https://twitter.com/morpheu5"><i class="fab fa-twitter text-3xl mr-2" alt="Twitter" style="color: #55acee"></i></a>
-				<a href="https://instagram.com/ilmoppe"><i class="fab fa-instagram text-3xl mr-3" alt="Instagram" style="color: #E1306C"></i></a>
-				<a href="https://www.flickr.com/photos/morpheu5"><i class="fab fa-flickr text-3xl mr-3" alt="Flickr" style="color: #0062DD"></i></a>
-				<a href="https://soundcloud.com/ilmoppe"><i class="fab fa-soundcloud text-3xl mr-3" alt="SoundClound" style="color: #FE5000"></i></a>
-				<a href="https://www.youtube.com/user/therealmorpheu5"><i class="fab fa-youtube text-3xl mr-3" alt="YouTube" style="color: #FF0000"></i></a>
-				<a href="https://morpheu5.deviantart.com"><i class="fab fa-deviantart text-3xl mr-3" alt="DeviantArt" style="color: #4DC47D"></i></a>
-			</div>
-		</section>
-		<section class="form">
-			<?php if((array_key_exists('contact_thanks', $_SESSION) && $_SESSION['contact_thanks'] == 1)) : ?>
-				<p class="contact_thanks">Thanks for your message, I'll try to answer as soon as I can!</p>
-				<p class="meanwhile">In the meanwhile, why don't you have a look at those links on the left?</p>
-				<?php unset($_SESSION['contact_thanks']) ?>
-			<?php else : ?>
-				<p>&nbsp;… or you can drop me a line.</p>
-				<form id="theContactForm" action="<?=$baseurl?>api/mailer" method="post">
-					<div class="text field flex flex-col mb-4" id="senderName">
-						<label for="senderNameBox" class="w-full pl-2">Name</label>
-						<input id="senderNameBox" class="w-full rounded border shadow-inner px-2" name="name" type="text" required autocomplete="name" placeholder="Charlie Doe" />
-					</div>
-					<div class="text field flex flex-col mb-4" id="senderEmail">
-						<label for="senderEmailBox" class="w-full pl-2">E-mail</label>
-						<input id="senderEmailBox" class="w-full rounded border shadow-inner px-2" name="email" type="email" required autocomplete="email" pattern="^.+@.+\..+" placeholder="charlie.doe@gmail.com"/>
-					</div>
-					<div class="textarea field flex-col mb-4" id="message">
-						<label for="messageBox" class="w-full pl-2">Message</label>
-						<textarea id="messageBox" class="w-full rounded border shadow-inner px-2" name="message" rows="5" required placeholder="Dear Evan Hansen, …"></textarea>
-					</div>
-					<div class="text field flex mb-4 hidden" id="captcha">
-						<label for="captchaBox">If you are human, you'll leave this box alone</label>
-						<input type="text" class="w-full rounded border shadow-inner px-2" id="captchaBox" name="captcha" placeholder="Dude…" />
-					</div>
-					
-					<div class="button" id="submit">
-						<button type="submit" id="submitButton" disabled><span class="fa fa-paper-plane mr-2"></span>Send</button>
-					</div>
-				</form>
-			<?php endif ?>
-		</section>
-	</article>
-		
-<?=i('_footer')?>

+ 72 - 0
pages/contact.vue

@@ -0,0 +1,72 @@
+<template><NuxtLayout name="default">
+    <template #headline>{{ headline }}</template>
+    <template #tagline>{{ tagline }}</template>
+
+    <div class="container mx-auto max-w-screen-lg sm:px-12 px-2 mb-12 prose">
+        <article class="body grid sm:grid-cols-2 grid-cols-1 gap-x-12 gap-y-0">
+            <section class="links">
+                <p>You can find me on these web sites…</p>
+                <div class="text-center ml-2">
+                    <a href="https://blog.morpheu5.net"><client-only><fa-icon icon="fa-solid fa-blog" alt="MorphLog" style="color: black" aria-hidden="true" /></client-only></a>
+                    <a href="https://orcid.org/0000-0001-8665-6147"><client-only><fa-icon icon="fa-brands fa-orcid" alt="ORCID" style="color: #a6cf39" aria-hidden="true" /></client-only></a>
+                    <a href="https://www.linkedin.com/in/andreafranceschini"><client-only><fa-icon icon="fa-brands fa-linkedin" alt="LinkedIn" style="color: #0077b5" aria-hidden="true" /></client-only></a>
+                    <a href="https://github.com/Morpheu5"><client-only><fa-icon icon="fa-brands fa-github" alt="GitHub" style="color: black" aria-hidden="true" /></client-only></a>
+                    <a href="https://apps.apple.com/it/developer/andrea-franceschini/id454062630"><client-only><fa-icon icon="fa-brands fa-app-store-ios" alt="App Store" style="color: #2296f3" aria-hidden="true" /></client-only></a>
+                    <a href="http://play.google.com/store/apps/developer?id=Andrea+Franceschini&amp;hl=it_IT"><client-only><fa-icon icon="fa-brands fa-google-play" alt="Google Play" style="color: #3BCCFF" aria-hidden="true" /></client-only></a>
+                </div>
+                <div class="mt-4 text-center">
+                    <a href="https://twitter.com/morpheu5"><client-only><fa-icon icon="fa-brands fa-twitter" alt="Twitter" style="color: #55acee" aria-hidden="true" /></client-only></a>
+                    <a href="https://instagram.com/ilmoppe"><client-only><fa-icon icon="fa-brands fa-instagram" alt="Instagram" style="color: #E1306C" aria-hidden="true" /></client-only></a>
+                    <a href="https://www.flickr.com/photos/morpheu5"><client-only><fa-icon icon="fa-brands fa-flickr" alt="Flickr" style="color: #0062DD" aria-hidden="true" /></client-only></a>
+                    <a href="https://soundcloud.com/ilmoppe"><client-only><fa-icon icon="fa-brands fa-soundcloud" alt="SoundClound" style="color: #FE5000" aria-hidden="true" /></client-only></a>
+                    <a href="https://www.youtube.com/user/therealmorpheu5"><client-only><fa-icon icon="fa-brands fa-youtube" alt="YouTube" style="color: #FF0000" aria-hidden="true" /></client-only></a>
+                    <a href="https://deviantart.com/morpheu5"><client-only><fa-icon icon="fa-brands fa-deviantart" alt="DeviantArt" style="color: #4DC47D" aria-hidden="true" /></client-only></a>
+                </div>
+            </section>
+            <section class="email">
+                <p>… or drop me an email at<br /><a :href="`mailto:${emailUser}\u0040${emailDomain}`"><client-only><fa-icon icon="fa fa-envelope" alt="Email" aria-hidden="true" /></client-only>&ensp;{{emailUser}}{{'\u0040'}}{{emailDomain}}</a></p>
+            </section>
+        </article>
+    </div>
+</NuxtLayout></template>
+
+<style lang="pcss" scoped>
+.links a {
+    @apply text-4xl;
+    @apply mr-6;
+
+    @media screen(sm) {
+        @apply text-5xl;
+    }
+}
+</style>
+
+<script lang="ts" setup>
+definePageMeta({
+    layout: false
+});
+const headline = "Contact";
+const tagline = "";
+
+const title = `${headline} ••• Andrea Franceschini, PhD`;
+const keywords = "contact, music, learning, tabletop, research, the open university, university of cambridge, isaac physics, reactable";
+const description = "Everything you need to know to get in touch with me.";
+
+useState('pageClasses', () => ['contact', 'page']);
+useHead({
+    title,
+    meta: [
+        { property: 'keywords', content: keywords },
+        { property: 'DC.Title', content: title },
+        { property: 'twitter:title', content: title },
+        { property: 'og:title', content: title },
+        { property: 'description', content: description },
+        { property: 'DC.Description', content: description },
+        { property: 'twitter:description', content: description },
+        { property: 'og:description', content: description },
+    ]
+});
+
+const emailUser = computed(() => '\u0061\u006e\u0064\u0072\u0065\u0061\u002e\u0066\u0072\u0061\u006e\u0063\u0065\u0073\u0063\u0068\u0069\u006e\u0069');
+const emailDomain = computed(() => '\u0067\u006d\u0061\u0069\u006c\u002e\u0063\u006f\u006d');
+</script>

+ 54 - 0
pages/cookies.vue

@@ -0,0 +1,54 @@
+<template><NuxtLayout name="default">
+    <template #headline>{{ headline }}</template>
+    <template #tagline>{{ tagline }}</template>
+
+    <div class="container mx-auto max-w-screen-lg sm:px-12 px-2 prose">
+        <article class="body grid sm:grid-cols-2 grid-cols-1 gap-x-12 gap-y-0">
+            <section class="col-span-1 sm:col-span-2">
+                <p class="noindent">Cookies are not always bad. Sometimes they are bad, sometimes they are useful.</p>
+                <p><strong>Good cookies</strong> are, for example, the ones that a website uses to store some useful
+                    information, like the fact that you logged in as a registered user, or decisions you made regarding
+                    your use of the site, like whether you prefer dark mode or light mode, or whether you are OK with the
+                    website using performance monitoring cookies. Good cookies are generally useful, helpful, and are
+                    <strong>not</strong> used to track you across the web and build a profile to be sold to advertisers.</p>
+                <p><strong>Bad cookies</strong> cookies do not have to be harmful or problematic, but can be. Such cookies
+                    can be, for example, tracking cookies used by a performance monitoring system to record how you use
+                    the website you are on, and can give important information to the manager of the website as to what
+                    works, what does not work, and how to improve.</p>
+                <p><strong>On this website</strong>, I use these:</p>
+                <p><strong>Consent</strong>: I record your preferences in the browser's local storage. This is tecnically
+                    not even a cookie but a better option with none of the bad side-effects of cookies. You can change
+                    your preferences at any time using the cookie dialog box that you can open clicking on the cookie
+                    icon in a corner at the bottom of the screen.</p>
+            </section>
+        </article>
+    </div>
+</NuxtLayout></template>
+
+<script lang="ts" setup>
+definePageMeta({
+    layout: false
+});
+
+const headline = "Cookies";
+const tagline = "They have them in the dark side";
+
+const title = `${headline} ••• Andrea Franceschini, PhD`;
+const description = "Everything you need to know to get in touch with me.";
+const keywords = "cookies, tracking, good, bad";
+
+useState('pageClasses', () => ['colophon', 'page']);
+useHead({
+    title,
+    meta: [
+        { property: 'keywords', content: keywords },
+        { property: 'DC.Title', content: title },
+        { property: 'twitter:title', content: title },
+        { property: 'og:title', content: title },
+        { property: 'description', content: description },
+        { property: 'DC.Description', content: description },
+        { property: 'twitter:description', content: description },
+        { property: 'og:description', content: description },
+    ]
+});
+</script>

+ 0 - 228
pages/cv.php

@@ -1,228 +0,0 @@
-<?php
-$bodyclasses = 'cv page';
-$title = 'Curriculum Vitæ';
-$tagline = '… what have I done?';
-$description = 'All the things you normally don\'t see about me, because the things one most tries to hide are often the things most easily seen.';
-$keywords = 'cv, curriculum vitae, resume, music, learning, tabletop, research';
-$twitter_image = 'images/colopic.jpg';
-?>
-
-<?=i('_header')?>
-
-	<article class="body grid grid-cols-1 print:grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-0">
-		<section id="personal" class="macro first full col-start-1 sm:col-span-2">
-			<h2>Personal information</h2>
-			<div class="body grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-0">
-				<section id="person_me" class="left pb-0" itemscope itemtype="http://schema.org/Person">
-					<p itemprop="name" class="hidden print:block">Andrea Franceschini, Ing. PhD</p>
-					<p itemprop="nationality" itemscope itemtype="http://schema.org/Country">Nationality: <span itemprop="name">Italian</span></p>
-					<meta itemprop="birthDate" content="1983-05-07" />
-					<meta itemprop="affiliation" content="Department of Computer Science and Technology, University of Cambridge, UK" />
-				</section>
-				<section id="full_cv" class="right print:hidden">
-					<p>Download the <a href="<?=$baseurl?>files/franceschini-andrea-cv.pdf" title="Full CV - PDF, 94 KB">full CV</a> (PDF, 94 KB)</p>
-					<p>Find me on <a href="//www.linkedin.com/in/andreafranceschini" target="_blank"><span class="fa fa-linkedin-square fa-fw" style="color: #0077b5 !important"></span> LinkedIn</a></p>
-				</section>
-			</div>
-		</section>
-
-		<section id="work" class="macro right col-start-1 col-span-1">
-			<h2>Work experience</h2>
-			<div class="body">
-				<section>
-					<p>July 2021 &mdash; present</p>
-					<p><strong>Visiting Fellow</strong></p>
-					<p><strong><a href="https://csc.dei.unipd.it">Centro di Sonologia Computazionale</a><br/>Universit&agrave; di Padova</strong>, Italy</p>
-					<p>Research and development for ArTracker, an IoT platform for tracking and monitoring artworks and cultural heritage.</p>
-				</section>
-				<section>
-					<p>January 2016 &mdash; present</p>
-					<p><strong>Post Doctoral Research Associate</strong></p>
-					<p><strong><a href="https://www.cst.cam.ac.uk">Department of Computer Science and Technology</a><br/>University of Cambridge</strong>, UK</p>
-					<p>Research and development for <a href="//www.isaaccomputerscience.org">Isaac Computer Science</a> and <a href="//www.isaacphysics.org">Isaac Physics</a>.</p>
-					<ul>
-						<li><strong>Development</strong>: <a href="https://isaacphysics.org/equality">a graphical, web-based entry system for symbolic formulas (maths, boolean logic, physical chemistry)</a>. Research on usability and impact on problem solving skills and physics learning.</li>
-						<li><strong>Research</strong>: analysis of impact based on <a href="https://www.compare-school-performance.service.gov.uk/">DfE exam results</a> and development of strategies to improve impact and engagement; learning outcome of different question formats; training and technical support for <a href="//www.asidatascience.com/">ASI Data Science Fellows</a> working on various projects</li>
-						<li><strong>Other</strong>: maintenance and infrastructure development</li>
-					</ul>
-				</section>
-				<section>
-					<p>November 2016 &mdash; present</p>
-					<p><strong>Post Doctoral Research Associate</strong></p>
-					<p><strong><a href="https://www.robinson.cam.ac.uk/">Robinson College</a><br/>University of Cambridge</strong>, UK</p>
-					<p>Teaching Supervisor, <a href="https://www.cl.cam.ac.uk/teaching/">Computer Science tripos</a></p>
-				</section>
-				<section>
-					<p>2006 &mdash; present</p>
-					<p><strong><a href="<?=$baseurl?>development">Freelance developer</a></strong> (iOS, Android, web)</p>
-					<ul>
-						<li><strong>Web sites</strong>, mostly for clients in the classical music industry</li>
-						<li><strong>Mobile applications</strong>, small- to mid-sized applications ranging from exhibition guides to citizen engagement, from interactive books to casual games</li>
-					</ul>
-					<p>Some <strong>technologies</strong>: iOS (Swift, Objective-C), Android (Java, Kotlin), Cinder (C++), Processing, Arduino, Drupal, Ruby on Rails, Vue.js, Nuxt.js, React</p>
-				</section>
-			</div>
-		</section>
-		<section id="education" class="macro left col-start-1 col-span-1 sm:col-start-2">
-			<h2>Education</h2>
-			<div class="body">
-				<section>
-					<p>April 2012 &mdash; February 2016</p>
-					<p><strong>PhD</strong> in Computing (HCI, Music Computing)</p>
-					<p><strong><a href="https://www.open.ac.uk">The Open University</a></strong>, UK</p>
-					<p>Thesis title: <em><a href="<?=$baseurl?>files/phd-thesis.pdf" title="PDF, 8.2 MB">Learning to use melodic similarity and contrast for narrative using a Digital Tabletop Musical Interface</a></em></p>
-					<p>supervisors: Dr Robin Laney, Mr Chris Dobbyn</p>
-				</section>
-				<section>
-					<p>December 2006 &mdash; June 2010</p>
-					<p><strong>Laurea Magistrale</strong> (MSc) in Computer Science and Engineering</p>
-					<p><strong><a href="http://www.unipd.it/">Università di Padova</a></strong>, IT</p>
-					<p>Thesis title: <em><a href="<?=$baseurl?>files/master_thesis.pdf" title="PDF, 706 KB">A practical approach to music theory on the Reactable</a></em></p>
-					<p>advisor: Prof. Giovanni De Poli</p>
-					<section class="indent">
-						<p>October 2008 &mdash; March 2009</p>
-						<p><strong>Erasmus</strong>, thesis research and development work on the <a href="//www.reactable.com">Reactable</a></p>
-						<p><strong><a href="https://www.upf.edu/web/mtg">Music Technology Group</a></strong></p>
-						<p><strong><a href="https://www.upf.edu/">Universitat Pompeu Fabra</a></strong>, ES</p>
-						<p>supervisor: Dr Sergi Jordà</p>
-					</section>
-				</section>
-				<section>
-					<p>October 2002 &mdash; December 2006</p>
-					<p><strong>Laurea</strong> (BSc) in Computer Science and Engineering</p>
-					<p><strong><a href="http://www.unipd.it/">Università di Padova</a></strong>, IT</p>
-					<p>Thesis title: <em>Schematizzazione di uno scheduler ciclico statico per applicazioni embedded in ambiente Windows CE</em></p>
-					<p>advisor: Dr Michele Moro</p>
-				</section>
-			</div>
-		</section>
-
-		<section id="awards" class="macro full col-start-2 sm:col-span-2">
-			<h2>Awards</h2>
-			<div class="body">
-				<section>
-					<p>May 2022</p>
-					<p><strong>MSCA 2021 <a href="<?=$baseurl?>files/msca-2021-seal-of-excellence.pdf" title="Seal of Excellence certificate">Seal of Excellence</a></strong></p>
-					<p>Marie Skłodowska-Curie Actions, European Commission</p>
-					<p>The Seal of Excellence is awarded to applications that rank more than 85% and do not get funded due to financial constraints.
-					   My application ranked <strong>92.8%</strong> with a funding threshold of 93.6%.</p>
-				</section>
-				<section>
-					<p>July 2021</p>
-					<p><strong><a href="http://csc.dei.unipd.it/artracker/">ArTracker</a> Visiting Fellowship</strong></p>
-					<p>Supported by Regione del Veneto, POR FSE 2014-2020, project number 2105-0051-1463-2019.</p>
-				</section>
-				<section>
-					<p>April 2015</p>
-					<p><strong>Full-time Research studentship (PhD) extension</strong></p>
-					<p>Faculty of Mathematics, Computing and Technology (MCT)</p>
-				</section>
-			</div>
-		</section>
-
-		<section id="teaching" class="macro full col-start-1 sm:col-span-2">
-			<h2>Teaching experience</h2>
-			<div class="body sm:float-left sm:w-1/2 sm:pr-2 print:float-none print:w-auto">
-				<h3>Courses</h3>
-				<section>
-					<p>January 2019 &mdash; March 2019</p>
-					<p><strong>Teaching Assistant</strong> at <strong><a href="https://www.cl.cam.ac.uk/">Computer Laboratory</a></strong><br/>
-					<strong>University of Cambridge</strong>, UK</p>
-					<p>Course: <strong><a href="https://www.cl.cam.ac.uk/teaching/1819/L312/">Digital Signal Processing with Computer Music</a></strong></p>
-				</section>
-				<section>
-					<p>October 2016 &mdash; present</p>
-					<p><strong>Teaching Supervisor</strong> at <strong><a href="https://www.cl.cam.ac.uk/">Computer Laboratory</a></strong><br/>
-					<strong>University of Cambridge</strong>, UK</p>
-					<p>Courses:</p>
-					<ul>
-						<li><a href="//www.cl.cam.ac.uk/teaching/1718/AdvGraph">Advanced Graphics</a></li>
-						<li><a href="//www.cl.cam.ac.uk/teaching/2021/FHCI">Further Human-Computer Interaction</a></li>
-						<li><a href="//www.cl.cam.ac.uk/teaching/1819/FJava">Further Java</a></li>
-						<li><a href="//www.cl.cam.ac.uk/teaching/2021/IntDesign">Interaction Design</a></li>
-						<li><a href="//www.cl.cam.ac.uk/teaching/1718/Graphics">Introduction to Graphics</a></li>
-						<li><a href="//www.cl.cam.ac.uk/teaching/1718/ProgC/">Programming in C</a></li>
-						<li><a href="//www.cl.cam.ac.uk/teaching/2021/ProgC/">Programming in C and C++</a></li>
-					</ul>
-				</section>
-				<section>
-					<p>October 2011 &mdash; November 2011</p>
-					<p><strong>Full-time teacher</strong> at <strong>Istituto di Istruzione Superiore di Lonigo</strong></p>
-					<p>Course: A035, Electrical Engineering and Applications</p>
-				</section>
-				<section>
-					<p>October 2002 &mdash; June 2010</p>
-					<p><strong>Private tutor</strong> for high school and university students</p>
-					<p>Courses: Mathematics, Computer Science, Electronics, Electrical Engineering</p>
-				</section>
-			</div>
-
-			<div class="body sm:float-right sm:w-1/2 sm:pl-2 print:float-none print:w-auto">
-				<h3>Students</h3>
-				<section>
-					<p>October 2019 &mdash; May 2020</p>
-					<p><strong>Sam Williams</strong>, Clare College</p>
-					<p><em>A Drum Rhythm Processing System to complement Sonic Pi</em></p>
-					<p>Part II (3<sup>rd</sup> year) dissertation, CompSci Tripos</p>
-				</section>
-				<section>
-					<p>October 2018 &mdash; May 2019</p>
-					<p><strong>Charles J. Y. Yoon</strong>, King's College</p>
-					<p><em>Musical Style Transfer using Neural Networks</em></p>
-					<p>Part II (3<sup>rd</sup> year) dissertation, CompSci Tripos</p>
-				</section>
-			</div>
-		</section>
-
-		<section class="macro full col-start-1 sm:col-span-2">
-			<h2>Publications</h2>
-			<div class="body grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-0">
-				<section id="ref-papers" class="full col-start-1 col-span-2">
-					<h3>Refereed papers</h3>
-					<ul class="nomargin">
-						<li>A. Franceschini, R. Laney, C. Dobbyn (2022) <a href="" title="This article is not published yet. A download link will be available soon after publication.">Sketching Music together: mixed groups exploring melodic similarity and contrast using a Digital Tabletop</a>. In <em>Journal of Music, Technology &amp; Education<!--, Volume 1x, Number y--></em> (Accepted for publication). <!-- (<a href="">Scopus</a>) --></li>
-						<li>T. Battistin, N. Dalla Pozza, S. Trentin, G. Volpin, A. Franceschini, RHF therapists team, A. Rodà (2022), <a href="" title="This article is not published yet. A download link will be available soon after publication.">Co-designed mini-games for children with visual impairment: a pilot study on their usability</a>. In <em>Multimedia Tools and Applications</em> (Accepted for publication).</li>
-						<li>J. Waite, A. Franceschini, S. Sentance, M. Patterson, J. Sharkey (2021), <a href="https://doi.org/10.1145/3481282.3481287">An online platform for teaching Upper Secondary school Computer Science</a>. In <em>Proceedings of the United Kingdom and Ireland Computing Education Research (UKICER 2021) Conference</em>. (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-85115215700&origin=resultslist">Scopus</a>)</li>
-						<li>A. Franceschini, R. Laney, C. Dobbyn (2020) <a href="https://doi.org/10.1386/jmte_00014_1">Sketching Music: Exploring melodic similarity and contrast using a Digital Tabletop</a>. In <em>Journal of Music, Technology &amp; Education, Volume 13, Number 1</em> (August 2020). (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-85099455819&origin=resultslist">Scopus</a>)</li>
-						<li>A. Franceschini, J. P. Sharkey, A. R. Beresford (2020) <a href="<?=$baseurl?>files/ccers20-inequality-poster-combo.pdf" title="PDF, 1.5 MB">Effective use of mathematical equations in an online learning environment</a>. In <em>Proceedings of the Cambridge Computing Education Research Symposium 2020</em>, Cambridge, UK.</li>
-						<li>A. Franceschini, J. P. Sharkey, A. R. Beresford (2019) <a href="<?=$baseurl?>files/las2019.pdf" title="PDF, 2 MB">Inequality: multi-modal equation entry on the web</a>. In <em>Proceedings of Learning @ Scale 2019</em>, Chicago, IL, USA. (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-85083953064&origin=resultslist">Scopus</a>)</li>
-						<li>A. Franceschini, R. Laney, C. Dobbyn (2016) <a href="<?=$baseurl?>files/met2016.pdf" title="PDF, 87 KB">Sketching music: making music through exploring art</a>. In <em>Proceedings of Sempre: Music, Education, Technology (MET2016)</em>, London, UK.</li>
-						<li>A. Franceschini, R. Laney, C. Dobbyn (2014) <a href="<?=$baseurl?>files/icmcsmc2014.pdf" title="PDF, 854 KB">Learning Musical Contour on a Tabletop</a>. In <em>Proceedings of the 40th International Computer Music Conference and 11th Sound and Music Computing Conference (ICMC-SMC)</em>, Athens, Greece. (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-84908884791&origin=resultslist">Scopus</a>)</li>
-						<li>A. Franceschini (2010) <a href="<?=$baseurl?>files/smc2010.pdf" title="PDF, 570 KB">Towards a practical approach to music theory on the Reactable</a>. In <em>Proceedings of the 7th Sound and Music Computing Conference (SMC)</em>, Barcelona, Spain. (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-84905169162&origin=resultslist">Scopus</a>)</li>
-					</ul>
-				</section>
-				<section id="posters" class="full col-start-1 col-span-2">
-					<h3>Posters</h3>
-					<ul class="nomargin">
-						<li>A. Franceschini, J. P. Sharkey, A. R. Beresford (2020) <a href="<?=$baseurl?>files/ccers20-inequality-poster-combo.pdf" title="PDF, 1.5 MB">Effective use of mathematical equations in an online learning environment</a>. In <em>Proceedings of the Cambridge Computing Education Research Symposium 2020</em>, Cambridge, UK.</li>
-						<li>A. Franceschini, R. Laney, C. Dobbyn (2014) <a href="<?=$baseurl?>files/icmcsmc2014-poster.pdf" title="PDF, 854 KB">Sketching Music</a>. <em>ICMC-SMC</em>, Athens, Greece.</li>
-						<li>A. Franceschini, R. Laney, C. Dobbyn (2013) <a href="<?=$baseurl?>files/dmrn8-poster-combo.pdf" title="PDF, 15 MB">A study of contour in music using digital tabletop musical instruments</a>. <em>DMRN+8</em>, London, UK.</li>
-					</ul>
-				</section>
-				<section id="talks" class="left col-start-1 col-span-1">
-					<h3>Talks</h3>
-					<ul class="nomargin">
-						<li><em><a href="<?=$baseurl?>files/csmc2017.pdf" title="PDF, 29 KB">Technologies and practices to “uncomplicate” music</a></em>. <a href="https://csmc2017.wordpress.com/">CSMC</a>, September 2017, The Open University, UK.</li>
-						<li><em>Qualitative Data Analysis</em>. Workshop, ESSD-HCI seminar series, March 2015, The Open University, UK.</li>
-						<li><em>Music lessons on a tabletop</em>. Invited seminar, 26 September 2014, Experimental Music Lab, Università di Trento, Italy.</li>
-					</ul>
-					<h3>Panels</h3>
-					<ul class="nomargin">
-						<li><em>Musical Creativity &amp; HCI</em>. <a href="https://csmc2017.wordpress.com/">CSMC</a>, September 2017, The Open University, UK.</li>
-					</ul>
-				</section>
-				<section id="mag-papers" class="right col-start-1 col-span-1 sm:col-start-2">
-					<h3>Magazine articles</h3>
-					<ul class="nomargin">
-						<li>A. Franceschini (2011) <a href="<?=$baseurl?>files/LC75_drupal.pdf">Estendere Drupal: scrivere moduli personalizzati</a>. In <em>Linux&amp;C</em>, 75. Piscopo Editore.</li>
-						<li>A. Franceschini (2011) <a href="<?=$baseurl?>files/LC74_drupal.pdf">Creare temi e pagine personalizzate per Drupal</a>. In <em>Linux&amp;C</em>, 74. Piscopo Editore.</li>
-						<li>A. Franceschini (2011) <a href="<?=$baseurl?>files/LC73_drupal.pdf">Drupal: web content manager potente e flessibile</a>. In <em>Linux&amp;C</em>, 73. Piscopo Editore.</li>
-						<li>A. Franceschini (2009) <a href="<?=$baseurl?>files/LC67_inkscape.pdf">Inkscape: impariamo a dominare tracciati e colori</a>. In <em>Linux&amp;C</em>, 67. Piscopo Editore.</li>
-						<li>A. Franceschini (2008) <a href="<?=$baseurl?>files/LC64_inkscape.pdf">Inkscape: fare grafica vettoriale con Linux</a>. In <em>Linux&amp;C</em>, 65. Piscopo Editore.</li>
-					</ul>
-				</section>
-			</div>
-		</section>
-	</article>
-
-<?=i('_footer')?>

+ 269 - 0
pages/cv.vue

@@ -0,0 +1,269 @@
+<template><NuxtLayout name="default">
+    <template #headline>{{ headline }}</template>
+    <template #tagline>{{ tagline }}</template>
+
+    <div class="container mx-auto max-w-screen-lg sm:px-12 px-2 prose">
+        <article class="body grid sm:grid-cols-2 grid-cols-1 gap-x-12 gap-y-0">
+            <section id="personal" class="macro sm:col-span-2">
+                <h2 class="sm:col-span-2">Personal information</h2>
+                <div class="body grid sm:grid-cols-2 gap-x-12">
+                    <section class="pb-0" itemscope itemtype="http://schema.org/Person">
+                        <p class="hidden print:block"><span itemprop="name">Andrea Franceschini</span>, <span itemprop="honorificPrefix">Ing.</span> <span itemprop="honorificSuffix">PhD</span></p>
+                        <p itemprop="nationality" itemtype="http://schema.org/Country">Nationality: <span itemprop="name">Italian</span></p>
+                        <meta itemprop="birthDate" content="1983-05-07">
+                        <meta itemprop="affiliation" content="Department of Computer Science and Technology, University of Cambridge, UK">
+                    </section>
+                    <section id="full_cv" class="print:hidden">
+                        <p>Download the <a href="/files/franceschini-andrea-cv.pdf" title="Full CV - PDF, 100 KB">full CV</a> (PDF, 100 KB)</p>
+                        <p>Find me on <a href="https://www.linkedin.com/in/andreafranceschini" target="_blank"><client-only><fa-icon icon="fa-brands fa-linkedin" style="color: #0077b5 !important" /></client-only> LinkedIn</a></p>
+                    </section>
+                </div>
+            </section>
+
+            <section id="work" class="macro">
+                <h2>Work experience</h2>
+                <div class="body">
+                    <section>
+                        <p>July 2021 — September 2021</p>
+                        <p><strong>Visiting Fellow</strong></p>
+                        <p><strong><a href="https://csc.dei.unipd.it">Centro di Sonologia Computazionale</a><br>Università di Padova</strong>, Italy</p>
+                        <p>Research and development for ArTracker, an IoT platform for tracking and monitoring artworks and cultural heritage.</p>
+                    </section>
+                    <section>
+                        <p>January 2016 — present</p>
+                        <p><strong>Post Doctoral Research Associate</strong></p>
+                        <p><strong><a href="https://www.cst.cam.ac.uk">Department of Computer Science and Technology</a><br>University of Cambridge</strong>, UK</p>
+                        <p>Research and development for <a href="https://www.isaaccomputerscience.org">Isaac Computer Science</a> and <a href="https://www.isaacphysics.org">Isaac Physics</a>.</p>
+                        <ul>
+                            <li><strong>Development</strong>: <a href="https://isaacphysics.org/equality">a graphical, web-based entry system for symbolic formulas (maths, boolean logic, physical chemistry)</a>. Research on usability and impact on problem solving skills and physics learning.</li>
+                            <li><strong>Research</strong>: analysis of impact based on <a href="https://www.compare-school-performance.service.gov.uk/">DfE exam results</a> and development of strategies to improve impact and engagement; learning outcome of different question formats; training and technical support for <a href="https://www.asidatascience.com/">ASI Data Science Fellows</a> working on various projects</li>
+                            <li><strong>Other</strong>: maintenance and infrastructure development</li>
+                        </ul>
+                    </section>
+                    <section>
+                        <p>November 2016 — present</p>
+                        <p><strong>Post Doctoral Research Associate</strong></p>
+                        <p><strong><a href="https://www.robinson.cam.ac.uk/">Robinson College</a><br>University of Cambridge</strong>, UK</p>
+                        <p>Teaching Supervisor, <a href="https://www.cl.cam.ac.uk/teaching/">Computer Science tripos</a></p>
+                    </section>
+                    <section>
+                        <p>2006 — present</p>
+                        <p><strong><a href="/development">Freelance developer</a></strong> (iOS, Android, web)</p>
+                        <ul>
+                            <li><strong>Web sites</strong>, mostly for clients in the classical music industry</li>
+                            <li><strong>Mobile applications</strong>, small- to mid-sized applications ranging from exhibition guides to citizen engagement, from interactive books to casual games</li>
+                        </ul>
+                        <p>Some <strong>technologies</strong>: iOS (Swift, Objective-C), Android (Java, Kotlin), Cinder (C++), Processing, Arduino, Drupal, Ruby on Rails, Vue.js, Nuxt.js, React</p>
+                    </section>
+                </div>
+            </section>
+            <section id="education" class="macro">
+                <h2>Education</h2>
+                <div class="body">
+                    <section>
+                        <p>April 2012 — February 2016</p>
+                        <p><strong>PhD</strong> in Computing (HCI, Music Computing)</p>
+                        <p><strong><a href="https://www.open.ac.uk">The Open University</a></strong>, UK</p>
+                        <p>Thesis title: <em><a href="/files/phd-thesis.pdf" title="PDF, 8.2 MB">Learning to use melodic similarity and contrast for narrative using a Digital Tabletop Musical Interface</a></em></p>
+                        <p>supervisors: Dr Robin Laney, Mr Chris Dobbyn</p>
+                    </section>
+                    <section>
+                        <p>December 2006 — June 2010</p>
+                        <p><strong>Laurea Magistrale</strong> (MSc) in Computer Science and Engineering</p>
+                        <p><strong><a href="http://www.unipd.it/">Università di Padova</a></strong>, IT</p>
+                        <p>Thesis title: <em><a href="/files/master_thesis.pdf" title="PDF, 706 KB">A practical approach to music theory on the Reactable</a></em></p>
+                        <p>advisor: Prof. Giovanni De Poli</p>
+                        <section class="indent">
+                            <p>October 2008 — March 2009</p>
+                            <p><strong>Erasmus</strong>, thesis research and development work on the <a href="https://www.reactable.com">Reactable</a></p>
+                            <p><strong><a href="https://www.upf.edu/web/mtg">Music Technology Group</a></strong></p>
+                            <p><strong><a href="https://www.upf.edu/">Universitat Pompeu Fabra</a></strong>, ES</p>
+                            <p>supervisor: Dr Sergi Jordà</p>
+                        </section>
+                    </section>
+                    <section>
+                        <p>October 2002 — December 2006</p>
+                        <p><strong>Laurea</strong> (BSc) in Computer Science and Engineering</p>
+                        <p><strong><a href="http://www.unipd.it/">Università di Padova</a></strong>, IT</p>
+                        <p>Thesis title: <em>Schematizzazione di uno scheduler ciclico statico per applicazioni embedded in ambiente Windows CE</em></p>
+                        <p>advisor: Dr Michele Moro</p>
+                    </section>
+                </div>
+            </section>
+
+            <section id="awards" class="macro sm:col-span-2">
+                <h2>Awards</h2>
+                <div class="body">
+                    <section>
+                        <p>July 2022</p>
+                        <p><strong>MSCA 2021 Individual Fellowship (EduGames)</strong></p>
+                        <p><a href="https://marie-sklodowska-curie-actions.ec.europa.eu/">Marie Skłodowska-Curie Actions</a>, European Commission</p>
+                        <p>An investigation into the role of ludic games in education.</p>
+                    </section>
+                    <section>
+                        <p>May 2022</p>
+                        <p><strong>MSCA 2021 <a href="/files/msca-2021-seal-of-excellence.pdf" title="Seal of Excellence certificate">Seal of Excellence</a></strong></p>
+                        <p>Marie Skłodowska-Curie Actions, European Commission</p>
+                        <p>The Seal of Excellence is awarded to applications that rank more than 85% and but cannot be funded in the first instance
+                            due to budget constraints. My application ranked <strong>92.8%</strong> with a funding threshold of 93.6%.</p>
+                    </section>
+                    <section>
+                        <p>July 2021</p>
+                        <p><strong><a href="http://csc.dei.unipd.it/artracker/">ArTracker</a> Visiting Fellowship</strong></p>
+                        <p>Supported by Regione del Veneto, POR FSE 2014-2020, project number 2105-0051-1463-2019.</p>
+                    </section>
+                    <section>
+                        <p>April 2015</p>
+                        <p><strong>Full-time Research studentship (PhD) extension</strong></p>
+                        <p>Faculty of Mathematics, Computing and Technology (MCT)</p>
+                    </section>
+                </div>
+            </section>
+
+            <section id="teaching" class="macro sm:col-span-2 grid sm:grid-cols-2 gap-x-12">
+                <h2 class="sm:col-span-2">Teaching experience</h2>
+                <div class="body">
+                    <h3>Courses</h3>
+                    <section>
+                        <p>January 2019 — March 2019</p>
+                        <p><strong>Teaching Assistant</strong> at <strong><a href="https://www.cl.cam.ac.uk/">Computer Laboratory</a></strong><br>
+                        <strong>University of Cambridge</strong>, UK</p>
+                        <p>Course: <strong><a href="https://www.cl.cam.ac.uk/teaching/1819/L312/">Digital Signal Processing with Computer Music</a></strong></p>
+                    </section>
+                    <section>
+                        <p>October 2016 — present</p>
+                        <p><strong>Teaching Supervisor</strong> at <strong><a href="https://www.cl.cam.ac.uk/">Computer Laboratory</a></strong><br>
+                        <strong>University of Cambridge</strong>, UK</p>
+                        <p>Courses:</p>
+                        <ul>
+                            <li><a href="https://www.cl.cam.ac.uk/teaching/1718/AdvGraph">Advanced Graphics</a></li>
+                            <li><a href="https://www.cl.cam.ac.uk/teaching/2021/FHCI">Further Human-Computer Interaction</a></li>
+                            <li><a href="https://www.cl.cam.ac.uk/teaching/1819/FJava">Further Java</a></li>
+                            <li><a href="https://www.cl.cam.ac.uk/teaching/2021/IntDesign">Interaction Design</a></li>
+                            <li><a href="https://www.cl.cam.ac.uk/teaching/1718/Graphics">Introduction to Graphics</a></li>
+                            <li><a href="https://www.cl.cam.ac.uk/teaching/1718/ProgC/">Programming in C</a></li>
+                            <li><a href="https://www.cl.cam.ac.uk/teaching/2021/ProgC/">Programming in C and C++</a></li>
+                        </ul>
+                    </section>
+                    <section>
+                        <p>October 2011 — November 2011</p>
+                        <p><strong>Full-time teacher</strong> at <strong>Istituto di Istruzione Superiore di Lonigo</strong></p>
+                        <p>Course: A035, Electrical Engineering and Applications</p>
+                    </section>
+                    <section>
+                        <p>October 2002 — June 2010</p>
+                        <p><strong>Private tutor</strong> for high school and university students</p>
+                        <p>Courses: Mathematics, Computer Science, Electronics, Electrical Engineering</p>
+                    </section>
+                </div>
+
+                <div class="body">
+                    <h3>Students</h3>
+                    <section>
+                        <p>October 2019 — May 2020</p>
+                        <p><strong>Sam Williams</strong>, Clare College, University of Cambridge</p>
+                        <p><em>A Drum Rhythm Processing System to complement Sonic Pi</em></p>
+                        <p>Part II (3<sup>rd</sup> year) dissertation, CompSci Tripos</p>
+                    </section>
+                    <section>
+                        <p>October 2018 — May 2019</p>
+                        <p><strong>Charles J. Y. Yoon</strong>, King's College, University of Cambridge</p>
+                        <p><em>Musical Style Transfer using Neural Networks</em></p>
+                        <p>Part II (3<sup>rd</sup> year) dissertation, CompSci Tripos</p>
+                    </section>
+                </div>
+            </section>
+
+            <section class="macro sm:col-span-2">
+                <h2>Publications</h2>
+                <div class="body">
+                    <section id="ref-papers">
+                        <h3>Refereed papers</h3>
+                        <ul class="nomargin">
+                            <li>(Accepted for publication) <span class="opacity-60">A. Franceschini, R. Laney, C. Dobbyn (2022) <a href="" title="This article is not published yet. A download link will be available soon after publication.">Sketching Music together: mixed groups exploring melodic similarity and contrast using a Digital Tabletop</a>. In <em>Journal of Music, Technology &amp; Education, Volume 14, Number 1-2</em>. <!-- (<a href="">Scopus</a>) --></span></li>
+                            <li>(Accepted for publication) <span class="opacity-60">T. Battistin, N. Dalla Pozza, S. Trentin, G. Volpin, A. Franceschini, RHF therapists team, A. Rodà (2022), <a href="" title="This article is not published yet. A download link will be available soon after publication.">Co-designed mini-games for children with visual impairment: a pilot study on their usability</a>. In <em>Multimedia Tools and Applications</em>.</span></li>
+                            <li>J. Waite, A. Franceschini, S. Sentance, M. Patterson, J. Sharkey (2021), <a href="https://doi.org/10.1145/3481282.3481287">An online platform for teaching Upper Secondary school Computer Science</a>. In <em>Proceedings of the United Kingdom and Ireland Computing Education Research (UKICER 2021) Conference</em>. (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-85115215700&amp;origin=resultslist">Scopus</a>)</li>
+                            <li>A. Franceschini, R. Laney, C. Dobbyn (2020) <a href="https://doi.org/10.1386/jmte_00014_1">Sketching Music: Exploring melodic similarity and contrast using a Digital Tabletop</a>. In <em>Journal of Music, Technology &amp; Education, Volume 13, Number 1</em> (August 2020). (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-85099455819&amp;origin=resultslist">Scopus</a>)</li>
+                            <li>A. Franceschini, J. P. Sharkey, A. R. Beresford (2020) <a href="/files/ccers20-inequality-poster-combo.pdf" title="PDF, 1.5 MB">Effective use of mathematical equations in an online learning environment</a>. In <em>Proceedings of the Cambridge Computing Education Research Symposium 2020</em>, Cambridge, UK.</li>
+                            <li>A. Franceschini, J. P. Sharkey, A. R. Beresford (2019) <a href="/files/las2019.pdf" title="PDF, 2 MB">Inequality: multi-modal equation entry on the web</a>. In <em>Proceedings of Learning @ Scale 2019</em>, Chicago, IL, USA. (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-85083953064&amp;origin=resultslist">Scopus</a>)</li>
+                            <li>A. Franceschini, R. Laney, C. Dobbyn (2016) <a href="/files/met2016.pdf" title="PDF, 87 KB">Sketching music: making music through exploring art</a>. In <em>Proceedings of Sempre: Music, Education, Technology (MET2016)</em>, London, UK.</li>
+                            <li>A. Franceschini, R. Laney, C. Dobbyn (2014) <a href="/files/icmcsmc2014.pdf" title="PDF, 854 KB">Learning Musical Contour on a Tabletop</a>. In <em>Proceedings of the 40th International Computer Music Conference and 11th Sound and Music Computing Conference (ICMC-SMC)</em>, Athens, Greece. (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-84908884791&amp;origin=resultslist">Scopus</a>)</li>
+                            <li>A. Franceschini (2010) <a href="/files/smc2010.pdf" title="PDF, 570 KB">Towards a practical approach to music theory on the Reactable</a>. In <em>Proceedings of the 7th Sound and Music Computing Conference (SMC)</em>, Barcelona, Spain. (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-84905169162&amp;origin=resultslist">Scopus</a>)</li>
+                        </ul>
+                    </section>
+                    <section id="posters">
+                        <h3>Posters</h3>
+                        <ul class="nomargin">
+                            <li>A. Franceschini, J. P. Sharkey, A. R. Beresford (2020) <a href="/files/ccers20-inequality-poster-combo.pdf" title="PDF, 1.5 MB">Effective use of mathematical equations in an online learning environment</a>. In <em>Proceedings of the Cambridge Computing Education Research Symposium 2020</em>, Cambridge, UK.</li>
+                            <li>A. Franceschini, R. Laney, C. Dobbyn (2014) <a href="/files/icmcsmc2014-poster.pdf" title="PDF, 854 KB">Sketching Music</a>. <em>ICMC-SMC</em>, Athens, Greece.</li>
+                            <li>A. Franceschini, R. Laney, C. Dobbyn (2013) <a href="/files/dmrn8-poster-combo.pdf" title="PDF, 15 MB">A study of contour in music using digital tabletop musical instruments</a>. <em>DMRN+8</em>, London, UK.</li>
+                        </ul>
+                    </section>
+                    <section id="talks">
+                        <h3>Talks</h3>
+                        <ul class="nomargin">
+                            <li><em><a href="/files/csmc2017.pdf" title="PDF, 29 KB">Technologies and practices to “uncomplicate” music</a></em>. <a href="https://csmc2017.wordpress.com/">CSMC</a>, September 2017, The Open University, UK.</li>
+                            <li><em>Qualitative Data Analysis</em>. Workshop, ESSD-HCI seminar series, March 2015, The Open University, UK.</li>
+                            <li><em>Music lessons on a tabletop</em>. Invited seminar, 26 September 2014, Experimental Music Lab, Università di Trento, Italy.</li>
+                        </ul>
+                        <h3>Panels</h3>
+                        <ul class="nomargin">
+                            <li><em>Musical Creativity &amp; HCI</em>. <a href="https://csmc2017.wordpress.com/">CSMC</a>, September 2017, The Open University, UK.</li>
+                        </ul>
+                    </section>
+                    <section id="mag-articles">
+                        <h3>Magazine articles</h3>
+                        <ul class="nomargin">
+                            <li>A. Franceschini (2011) <a href="/files/LC75_drupal.pdf">Estendere Drupal: scrivere moduli personalizzati</a>. In <em>Linux&amp;C</em>, 75. Piscopo Editore.</li>
+                            <li>A. Franceschini (2011) <a href="/files/LC74_drupal.pdf">Creare temi e pagine personalizzate per Drupal</a>. In <em>Linux&amp;C</em>, 74. Piscopo Editore.</li>
+                            <li>A. Franceschini (2011) <a href="/files/LC73_drupal.pdf">Drupal: web content manager potente e flessibile</a>. In <em>Linux&amp;C</em>, 73. Piscopo Editore.</li>
+                            <li>A. Franceschini (2009) <a href="/files/LC67_inkscape.pdf">Inkscape: impariamo a dominare tracciati e colori</a>. In <em>Linux&amp;C</em>, 67. Piscopo Editore.</li>
+                            <li>A. Franceschini (2008) <a href="/files/LC64_inkscape.pdf">Inkscape: fare grafica vettoriale con Linux</a>. In <em>Linux&amp;C</em>, 65. Piscopo Editore.</li>
+                        </ul>
+                    </section>
+                </div>
+            </section>
+        </article>
+    </div>
+</NuxtLayout></template>
+
+<style lang="pcss" scoped>
+.prose {
+    p {
+        margin-top: 0;
+        margin-bottom: 0;
+    }
+
+    .body > section {
+        padding-bottom: theme(spacing.6);
+    }
+}
+</style>
+
+<script lang="ts" setup>
+definePageMeta({
+    layout: false
+});
+
+const headline = "Curriculum Vitæ";
+const tagline = "What have I done?";
+
+const title = `${headline} ••• Andrea Franceschini, PhD`;
+const keywords = "cv, curriculum vitae, resume, music, learning, tabletop, research, the open university, university of cambridge, isaac physics, reactable";
+const description = "All the things you normally don't see about me, because the things one most tries to hide are often the things most easily seen.";
+
+useState('pageClasses', () => ['cv', 'page']);
+useHead({
+    title,
+    meta: [
+        { property: 'keywords', content: keywords },
+        { property: 'DC.Title', content: title },
+        { property: 'twitter:title', content: title },
+        { property: 'og:title', content: title },
+        { property: 'description', content: description },
+        { property: 'DC.Description', content: description },
+        { property: 'twitter:description', content: description },
+        { property: 'og:description', content: description },
+    ]
+});
+</script>

+ 0 - 238
pages/development.php

@@ -1,238 +0,0 @@
-<?php
-$bodyclasses = 'development page';
-$title = 'Software development';
-$tagline = 'not black magic';
-$description = 'I design software and write code. Yes, I make apps.';
-$keywords = 'music, learning, tabletop, research, the open university';
-$twitter_image = 'images/phd-app-2x.jpg';
-?>
-
-<?=i('_header')?>
-
-	<article class="body grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-0">
-		<section class="full col-start-1 sm:col-span-2">
-			<p>I have been writing code for as long as I can remember &ndash; yes, that must have been around 1990, with my Amiga 500. It has been a long journey, and I learned a thing or two along the way.</p>
-		</section>
-		<section class="left col-start-1 col-span-1">
-			<p>One of these is that the “<em>what languages do you know?</em>” question is largely irrelevant. Once you know a bunch of them, learning a new one is easy. The hard part is knowing how to attack a problem and solve it.</p>
-		</section>
-		<section class="right col-start-1 col-span-1 sm:col-start-2">
-			<p><strong>Languages</strong> I know:</p>
-			<ul>
-				<li>experienced: C, C++(11, 14…), Objective-C, XML, HTML, TypeScript, PHP, Ruby, Pure Data, Processing</li>
-				<li>familiar: Swift, JavaScript, Python, x86 and ARM assembly for system programming</li>
-				<li>learning: R, Julia, Haskell</li>
-			</ul>
-		</section>
-		<section class="left col-start-1 col-span-1">
-			<p>Knowing languages is only a small portion of the job. You also need to know how to not <a href="//en.wikipedia.org/wiki/Reinventing_the_wheel">reinvent the wheel</a> all the time, how to design maintainable and scalable software architectures, and so on.</p>
-		</section>
-		<section class="right col-start-1 col-span-1 sm:col-start-2">
-			<p><strong>Frameworks</strong> and other magic tricks:</p>
-			<ul>
-				<li>Qt, Cinder, openFrameworks</li>
-				<li>Ruby on Rails, Sinatra</li>
-				<li>React, Vue.js, Nuxt.js</li>
-				<li>Arduino</li>
-			</ul>
-		</section>
-		<section class="full col-start-1 sm:col-span-2">
-			<p>I may be forgetting some technologies, but the bottom line is: <strong>it's no use knowing stuff if you don't know what it's used for</strong>.</p>
-			<h2>Things I've done</h2>
-			<p>Enough talk, let's see some of the projects I worked on over the years.</p>
-		</section>
-		<section class="project full col-start-1 sm:col-span-2 border-t-2 pb-3" id="project_rfk2d">
-			<section class="_left sm:float-left sm:w-1/2 sm:pr-1">
-				<h3>Robot Finds Kitten 2D for Godot Engine</h3>
-			</section>
-			<section class="_right sm:float-right sm:w-1/2 sm:pl-2">
-				<figure>
-					<a href="#" data-featherlight="<?=$baseurl?>images/rfk2d-2x.jpg">
-						<picture>
-							<source srcset="<?=$baseurl?>images/rfk2d-2x.webp" type="image/webp" />
-							<source srcset="<?=$baseurl?>images/rfk2d.jpg 1x, <?=$baseurl?>images/rfk2d-2x.jpg 2x" type="image/jpeg" />
-							<img class="rounded" srcset="<?=$baseurl?>images/rfk2d.jpg" alt="Robot Finds Kitten 2D for Godot Engine" />
-						</picture>
-					</a>
-				</figure>
-			</section>
-			<section class="_left sm:float-left sm:w-1/2 sm:pr-1">
-				<p>I wanted to learn <a href="https://godotengine.org/">Godot</a> so I made a port of the <a href="http://robotfindskitten.org/">famous zen simulation</a> Robot Finds Kitten which you can <a href="https://rfk.morpheu5.net/2d" title="Play Robot Finds Kitten 2D online for free!">play online here</a> for free! No ads! Pure zen!<p>
-				<p><strong>Technologies</strong>: Godot Engine.</p>
-				<p><a href="//www.github.com/Morpheu5/RFK_Godot2D"><strong>Source code</strong></a> | <a href="https://rfk.morpheu5.net/2d" title="Play Robot Finds Kitten 2D online for free!"><strong>Play</strong></a></p>
-			</section>
-		</section>
-		<section class="project full col-start-1 sm:col-span-2 pb-3" id="project_circularbells">
-			<section class="_right sm:float-right sm:w-1/2 sm:pl-1">
-				<h3>Circular Bells</h3>
-			</section>
-			<section class="_left sm:float-left sm:w-1/2 sm:pr-2">
-				<figure>
-					<a href="#" data-featherlight="<?=$baseurl?>images/cb_demo-2x.jpg">
-						<picture>
-							<source srcset="<?=$baseurl?>images/cb_demo-2x.webp" type="image/webp" />
-							<source srcset="<?=$baseurl?>images/cb_demo.jpg 1x, <?=$baseurl?>images/cb_demo-2x.jpg 2x" type="image/jpeg" />
-							<img class="rounded" srcset="<?=$baseurl?>images/cb_demo.jpg" alt="Circular Bells" />
-						</picture>
-					</a>
-				</figure>
-			</section>
-			<section class="_right sm:float-right sm:w-1/2 sm:pl-1">
-				<p>Circular Bells is a music app that for kids and grown ups to sit back and relax while playing with its brightly-coloured, ever-changing interface, and uplifting sounds.</p>
-				<p><strong>Technologies</strong>: C++, Cinder, iOS.</p>
-				<p><a href="//itunes.apple.com/us/app/circular-bells/id1062362784?mt=8"><strong>App Store</strong></a> | <a href="//www.youtube.com/watch?v=a_GSQNIT7xM"><strong>Demo</strong></a></p>
-			</section>
-		</section>
-		<section class="project full col-start-1 sm:col-span-2 border-t-2 pb-3" id="project_phdappios">
-			<section class="_left sm:float-left sm:w-1/2 sm:pr-1">
-				<h3>PhD music app for iPad</h3>
-			</section>
-			<section class="_right sm:float-right sm:w-1/2 sm:pl-2">
-				<figure>
-					<a href="#" data-featherlight="<?=$baseurl?>images/phd-app-2x.jpg">
-						<picture>
-							<source srcset="<?=$baseurl?>images/phd-app-2x.webp" type="image/webp" />
-							<source srcset="<?=$baseurl?>images/phd-app.jpg 1x, <?=$baseurl?>images/phd-app-2x.jpg 2x" type="image/jpeg" />
-							<img class="rounded" srcset="<?=$baseurl?>images/phd-app.jpg" alt="PhD app for iPad" />
-						</picture>
-					</a>
-				</figure>
-			</section>
-			<section class="_left sm:float-left sm:w-1/2 sm:pr-1">
-				<p>In 2014, I needed to bring the app below with me to a conference for demos. I figured pretty soon that I was not going to bring the giant screen with me, so I did an iOS port which worked wonderfully. No, you can't have it. Yet. I'm designing a new version. You can have that once it's ready.<p>
-				<p><strong>Technologies</strong>: C++, Cinder, iOS.</p>
-				<p><a href="//www.github.com/Morpheu5/SecondStudy-iPad"><strong>Source code</strong></a></p>
-			</section>
-		</section>
-		<section class="project full col-start-1 sm:col-span-2 border-t-2 pb-3" id="project_phdapp">
-			<section class="_right sm:float-right sm:w-1/2 sm:pl-1">
-				<h3>PhD music app</h3>
-			</section>
-			<section class="_left sm:float-left sm:w-1/2 sm:pr-2">
-				<figure>
-					<a href="#" data-featherlight="<?=$baseurl?>images/table_2-2x.jpg">
-						<picture>
-							<source srcset="<?=$baseurl?>images/table_2-2x.webp" type="image/webp" />
-							<source srcset="<?=$baseurl?>images/table_2.jpg 1x, <?=$baseurl?>images/table_2-2x.jpg 2x" type="image/jpeg" />
-							<img class="rounded" srcset="<?=$baseurl?>images/table_2.jpg" alt="PhD app for large tabletop" />
-						</picture>
-					</a>
-				</figure>
-			</section>
-			<section class="_right sm:float-right sm:w-1/2 sm:pl-1">
-				<p>As part of my doctoral studies, I developed a collaborative music composition tabletop application to teach people how to compose and discuss melodies.</p>
-				<p><strong>Technologies</strong>: C++, Cinder, Pure Data.</p>
-				<p><a href="//www.github.com/Morpheu5/FourthStudy"><strong>Source code</strong></a> | <a href="//www.youtube.com/watch?v=AONzmnAHZ6w"><strong>Demo</strong></a></p>
-			</section>
-		</section>
-		<section class="project full col-span-2 border-t-2 pb-3" id="project_aranaboo">
-			<section class="_left sm:float-left sm:w-1/2 sm:pr-1">
-				<h3>Aranaboo</h3>
-			</section>
-			<section class="_right sm:float-right sm:w-1/2 sm:pl-2">
-				<figure>
-					<a href="#" data-featherlight="<?=$baseurl?>images/aranaboo-2x.jpg">
-						<picture>
-							<source srcset="<?=$baseurl?>images/aranaboo-2x.webp" type="image/webp" type="image/webp" />
-							<source srcset="<?=$baseurl?>images/aranaboo.jpg 1x, <?=$baseurl?>images/aranaboo-2x.jpg 2x" type="image/jpeg" />
-							<img class="rounded" srcset="<?=$baseurl?>images/aranaboo.jpg" alt="Aranaboo" />
-						</picture>
-					</a>
-				</figure>
-			</section>
-			<section class="_left sm:float-left sm:w-1/2 sm:pr-1">
-				<p>A game of peek-a-boo using face detection.<p>
-				<p><strong>Technologies</strong>: iOS.</p>
-				<p><a href="//www.beyoucompany.com/?page_id=8"><strong>Web site</strong></a></p>
-			</section>
-		</section>
-		<section class="project full col-start-1 sm:col-span-2 border-t-2 pb-3" id="project_astronauta">
-			<section class="_right sm:float-right sm:w-1/2 sm:pl-1">
-				<h3>Da grande voglio fare l'astronauta</h3>
-			</section>
-			<section class="_left sm:float-left sm:w-1/2 sm:pr-2">
-				<figure>
-					<a href="#" data-featherlight="<?=$baseurl?>images/astronauta-2x.jpg">
-						<picture>
-							<source srcset="<?=$baseurl?>images/astronauta-2x.webp" type="image/webp" />
-							<source srcset="<?=$baseurl?>images/astronauta.jpg 1x, <?=$baseurl?>images/astronauta-2x.jpg 2x" type="image/jpeg" />
-							<img class="rounded" srcset="<?=$baseurl?>images/astronauta.jpg" alt="Da grande voglio fare l'astronauta" />
-						</picture>
-					</a>
-				</figure>
-			</section>
-			<section class="_right sm:float-right sm:w-1/2 sm:pl-1">
-				<p>An interactive book for children. The tale of a kid who wants to become an astronaut. I developed the iOS part of the project to completion, but sadly the app was never released.</p>
-				<p><strong>Technologies</strong>: iOS.</p>
-				<p><a href="//www.behance.net/gallery/1201947/DA-GRANDE-VOGLIO-FARE-LASTRONAUTA-(-IPAD-APP)"><strong>Behance</strong></a></p>
-			</section>
-		</section>
-		<section class="project full col-start-1 sm:col-span-2 border-t-2 pb-3" id="project_bsc">
-			<section class="_left sm:float-left sm:w-1/2 sm:pr-1">
-				<h3>CRUSADE Bleeding Score Calculator</h3>
-			</section>
-			<section class="_right sm:float-right sm:w-1/2 sm:pl-2">
-				<figure>
-					<a href="#" data-featherlight="<?=$baseurl?>images/bsc-2x.jpg">
-						<picture>
-							<source srcset="<?=$baseurl?>images/bsc-2x.webp" type="image/webp" />
-							<source srcset="<?=$baseurl?>images/bsc.jpg 1x, <?=$baseurl?>images/bsc-2x.jpg 2x" type="image/jpeg" />
-							<img class="rounded" srcset="<?=$baseurl?>images/bsc.png" alt="CRUSADE Bleeding Score Calculator" />
-						</picture>
-					</a>
-				</figure>
-			</section>
-			<section class="_left sm:float-left sm:w-1/2 sm:pr-1">
-				<p>This app helps clinicians estimate a patient's baseline risk of in-hospital major bleeding during both ST and non-ST segment elevation myocardial infarction. Not that I know what that means. I was given a spreadsheet with some formulas, I was asked to make an iOS app, and so I did. Then the client did not want it anymore, so I published it myself.</p>
-				<p><strong>Technologies</strong>: iOS (Swift), Android (Kotlin).</p>
-				<p><a href="//itunes.apple.com/us/app/crusade-bleeding-score-calculator/id454062627?mt=8"><strong>App Store</strong></a>, <a href="//play.google.com/store/apps/details?id=net.morpheu5.bleedingscorecalculator"><strong>Google Play</strong></a></p>
-			</section>
-		</section>
-		<section class="project full col-start-1 sm:col-span-2 border-t-2 pb-3" id="project_doodle">
-			<section class="_right sm:float-right sm:w-1/2 sm:pl-1">
-				<h3>Doodle</h3>
-			</section>
-			<section class="_left sm:float-left sm:w-1/2 sm:pr-2">
-				<figure>
-					<a href="#" data-featherlight="<?=$baseurl?>images/doodle-2x.png">
-						<picture>
-							<source srcset="<?=$baseurl?>images/doodle-2x.webp" type="image/webp" />
-							<source srcset="<?=$baseurl?>images/doodle.png 1x, <?=$baseurl?>images/doodle-2x.png 2x" type="image/png" />
-							<img class="rounded" srcset="<?=$baseurl?>images/doodle.png" alt="Doodle" />
-						</picture>
-					</a>
-				</figure>
-			</section>
-			<section class="_right sm:float-right sm:w-1/2 sm:pl-1">
-				<p>I worked on the actual Reactable between 2008 and 2009, proposing a new approach to Western tonal music &ndash; the original support was quite minimal, I was tasked with making it a bit more usable.</p>
-				<p><strong>Technologies</strong>: C++, <a href="//www.qt.io">Qt 4</a>, <a href="//www.jackaudio.org">Jack</a>.</p>
-				<p><a href="//www.github.com/Morpheu5/Doodle"><strong>Source code</strong></a> | <a href="//vimeo.com/4325822"><strong>Demo</strong></a></p>
-			</section>
-		</section>
-		<section class="project full col-start-1 sm:col-span-2 border-t-2" id="project_tofus">
-			<section class="_left sm:float-left sm:w-1/2 sm:pr-1">
-				<h3>tOfuS: the Framework OS</h3>
-			</section>
-			<section class="_right sm:float-right sm:w-1/2 sm:pl-2">
-				<figure>
-					<a href="#" data-featherlight="<?=$baseurl?>images/tofus-2x.jpg">
-						<picture>
-							<source srcset="<?=$baseurl?>images/tofus-2x.webp" type="image/webp" />
-							<source srcset="<?=$baseurl?>images/tofus.png 1x, <?=$baseurl?>images/tofus-2x.png 2x" type="image/png" />
-							<img class="rounded" srcset="<?=$baseurl?>images/tofus.jpg" alt="tOfuS" />
-						</picture>
-					</a>
-				</figure>
-			</section>
-			<section class="_left sm:float-left sm:w-1/2 sm:pr-1">
-				<p>Yes, there was a time when I worked on a toy operating system. It was good fun and I learned a lot about assembly code and bare-metal programming.</p>
-				<p><strong>Technologies</strong>: x86 assembly, C.</p>
-				<p><a href="//www.github.com/Morpheu5/tOfuS"><strong>Source code</strong></a></p>
-			</section>
-		</section>
-		<section class="full col-start-1 sm:col-span-2">
-			<p class="calltoaction">Have a cool project in mind? <a href="<?=$baseurl?>contact">Let's talk about it!</a></p>
-		</section>
-	</article>
-
-<?=i('_footer')?>

+ 261 - 0
pages/development.vue

@@ -0,0 +1,261 @@
+<template><NuxtLayout name="default">
+    <template #headline>{{ headline }}</template>
+    <template #tagline>{{ tagline }}</template>
+
+    <div class="container mx-auto max-w-screen-lg sm:px-12 px-2 prose">
+        <article class="body grid sm:grid-cols-2 grid-cols-1 gap-x-12 gap-y-0">
+            <section class="full col-start-1 sm:col-span-2">
+                <p>I have been writing code for as long as I can remember &ndash; yes, that must have been around 1990, with my Amiga 500. It has been a long journey, and I learned a thing or two along the way.</p>
+            </section>
+            <section class="left col-start-1 col-span-1">
+                <p>One of these is that the “<em>what languages do you know?</em>” question is largely irrelevant. Once you know a bunch of them, learning a new one is easy. The hard part is knowing how to attack a problem and solve it.</p>
+            </section>
+            <section class="right col-start-1 col-span-1 sm:col-start-2">
+                <p><strong>Languages</strong> I know:</p>
+                <ul>
+                    <li>experienced: C, C++, Objective-C, XML, HTML, TypeScript, PHP, Ruby, Pure Data, Processing</li>
+                    <li>familiar: Swift, JavaScript, Python, x86 and ARM assembly for system programming</li>
+                    <li>learning: R, Julia, Haskell</li>
+                </ul>
+            </section>
+            <section class="left col-start-1 col-span-1">
+                <p>Knowing languages is only a small portion of the job. You also need to know how to not <a href="https://en.wikipedia.org/wiki/Reinventing_the_wheel">reinvent the wheel</a> all the time, how to design maintainable and scalable software architectures, and so on.</p>
+            </section>
+            <section class="right col-start-1 col-span-1 sm:col-start-2">
+                <p><strong>Frameworks</strong> and other magic tricks:</p>
+                <ul>
+                    <li>Qt, Cinder, openFrameworks</li>
+                    <li>Ruby on Rails, Sinatra</li>
+                    <li>React, Vue.js, Nuxt.js</li>
+                    <li>Arduino</li>
+                    <li>Godot</li>
+                </ul>
+            </section>
+            <section class="full col-start-1 sm:col-span-2">
+                <p>I may be forgetting some technologies, but the bottom line is: <strong>it's no use knowing stuff if you don't know what it's used for</strong>.</p>
+                <h2>Things I've done</h2>
+                <p>Enough talk, let's see some of the projects I worked on over the years.</p>
+            </section>
+            <section class="project full col-start-1 sm:col-span-2 border-t-2 pb-3" id="project_rfk2d">
+                <section class="_left sm:float-left sm:w-1/2 sm:pr-1">
+                    <h3>Robot Finds Kitten 2D for Godot Engine</h3>
+                </section>
+                <section class="_right sm:float-right sm:w-1/2 sm:pl-2">
+                    <figure>
+                        <a href="#" data-featherlight="/images/rfk2d-2x.jpg">
+                            <picture>
+                                <source srcset="/images/rfk2d-2x.webp" type="image/webp" />
+                                <source srcset="/images/rfk2d.jpg 1x, /images/rfk2d-2x.jpg 2x" type="image/jpeg" />
+                                <img class="rounded" srcset="/images/rfk2d.jpg" alt="Robot Finds Kitten 2D for Godot Engine" />
+                            </picture>
+                        </a>
+                    </figure>
+                </section>
+                <section class="_left sm:float-left sm:w-1/2 sm:pr-1">
+                    <p>I wanted to learn <a href="https://godotengine.org/">Godot</a> so I made a port of the <a href="http://robotfindskitten.org/">famous zen simulation</a> Robot Finds Kitten which you can <a href="https://rfk.morpheu5.net/2d" title="Play Robot Finds Kitten 2D online for free!">play online here</a> for free! No ads! Pure zen!</p>
+                    <p><strong>Technologies</strong>: Godot Engine.</p>
+                    <p><a href="https://www.github.com/Morpheu5/RFK_Godot2D"><strong>Source code</strong></a> | <a href="https://rfk.morpheu5.net/2d" title="Play Robot Finds Kitten 2D online for free!"><strong>Play</strong></a></p>
+                </section>
+            </section>
+            <section class="project full col-start-1 sm:col-span-2 border-t-2 pb-3" id="project_circularbells">
+                <section class="_right sm:float-right sm:w-1/2 sm:pl-1">
+                    <h3>Circular Bells</h3>
+                </section>
+                <section class="_left sm:float-left sm:w-1/2 sm:pr-2">
+                    <figure>
+                        <a href="#" data-featherlight="/images/cb_demo-2x.jpg">
+                            <picture>
+                                <source srcset="/images/cb_demo-2x.webp" type="image/webp" />
+                                <source srcset="/images/cb_demo.jpg 1x, /images/cb_demo-2x.jpg 2x" type="image/jpeg" />
+                                <img class="rounded" srcset="/images/cb_demo.jpg" alt="Circular Bells" />
+                            </picture>
+                        </a>
+                    </figure>
+                </section>
+                <section class="_right sm:float-right sm:w-1/2 sm:pl-1">
+                    <p>Circular Bells is a music app that for kids and grown ups to sit back and relax while playing with its brightly-coloured, ever-changing interface, and uplifting sounds.</p>
+                    <p><strong>Technologies</strong>: C++, Cinder, iOS.</p>
+                    <p><a href="https://itunes.apple.com/us/app/circular-bells/id1062362784?mt=8"><strong>App Store</strong></a> | <a href="https://www.youtube.com/watch?v=a_GSQNIT7xM"><strong>Demo</strong></a></p>
+                </section>
+            </section>
+            <section class="project full col-start-1 sm:col-span-2 border-t-2 pb-3" id="project_phdappios">
+                <section class="_left sm:float-left sm:w-1/2 sm:pr-1">
+                    <h3>PhD music app for iPad</h3>
+                </section>
+                <section class="_right sm:float-right sm:w-1/2 sm:pl-2">
+                    <figure>
+                        <a href="#" data-featherlight="/images/phd-app-2x.jpg">
+                            <picture>
+                                <source srcset="/images/phd-app-2x.webp" type="image/webp" />
+                                <source srcset="/images/phd-app.jpg 1x, /images/phd-app-2x.jpg 2x" type="image/jpeg" />
+                                <img class="rounded" srcset="/images/phd-app.jpg" alt="PhD app for iPad" />
+                            </picture>
+                        </a>
+                    </figure>
+                </section>
+                <section class="_left sm:float-left sm:w-1/2 sm:pr-1">
+                    <p>In 2014, I needed to bring the app below with me to a conference for demos. I figured pretty soon that I was not going to bring the giant screen with me, so I did an iOS port which worked wonderfully. No, you can't have it. Yet. I'm designing a new version. You can have that once it's ready.</p>
+                    <p><strong>Technologies</strong>: C++, Cinder, iOS.</p>
+                    <p><a href="https://www.github.com/Morpheu5/SecondStudy-iPad"><strong>Source code</strong></a></p>
+                </section>
+            </section>
+            <section class="project full col-start-1 sm:col-span-2 border-t-2 pb-3" id="project_phdapp">
+                <section class="_right sm:float-right sm:w-1/2 sm:pl-1">
+                    <h3>PhD music app</h3>
+                </section>
+                <section class="_left sm:float-left sm:w-1/2 sm:pr-2">
+                    <figure>
+                        <a href="#" data-featherlight="/images/table_2-2x.jpg">
+                            <picture>
+                                <source srcset="/images/table_2-2x.webp" type="image/webp" />
+                                <source srcset="/images/table_2.jpg 1x, /images/table_2-2x.jpg 2x" type="image/jpeg" />
+                                <img class="rounded" srcset="/images/table_2.jpg" alt="PhD app for large tabletop" />
+                            </picture>
+                        </a>
+                    </figure>
+                </section>
+                <section class="_right sm:float-right sm:w-1/2 sm:pl-1">
+                    <p>As part of my doctoral studies, I developed a collaborative music composition tabletop application to teach people how to compose and discuss melodies.</p>
+                    <p><strong>Technologies</strong>: C++, Cinder, Pure Data.</p>
+                    <p><a href="https://www.github.com/Morpheu5/FourthStudy"><strong>Source code</strong></a> | <a href="https://www.youtube.com/watch?v=AONzmnAHZ6w"><strong>Demo</strong></a></p>
+                </section>
+            </section>
+            <section class="project full col-span-2 border-t-2 pb-3" id="project_aranaboo">
+                <section class="_left sm:float-left sm:w-1/2 sm:pr-1">
+                    <h3>Aranaboo</h3>
+                </section>
+                <section class="_right sm:float-right sm:w-1/2 sm:pl-2">
+                    <figure>
+                        <a href="#" data-featherlight="/images/aranaboo-2x.jpg">
+                            <picture>
+                                <source srcset="/images/aranaboo-2x.webp" type="image/webp" />
+                                <source srcset="/images/aranaboo.jpg 1x, /images/aranaboo-2x.jpg 2x" type="image/jpeg" />
+                                <img class="rounded" srcset="/images/aranaboo.jpg" alt="Aranaboo" />
+                            </picture>
+                        </a>
+                    </figure>
+                </section>
+                <section class="_left sm:float-left sm:w-1/2 sm:pr-1">
+                    <p>A game of peek-a-boo using face detection.</p>
+                    <p><strong>Technologies</strong>: iOS.</p>
+                    <p><a href="https://www.beyoucompany.com/?page_id=8"><strong>Web site</strong></a></p>
+                </section>
+            </section>
+            <section class="project full col-start-1 sm:col-span-2 border-t-2 pb-3" id="project_astronauta">
+                <section class="_right sm:float-right sm:w-1/2 sm:pl-1">
+                    <h3>Da grande voglio fare l'astronauta</h3>
+                </section>
+                <section class="_left sm:float-left sm:w-1/2 sm:pr-2">
+                    <figure>
+                        <a href="#" data-featherlight="/images/astronauta-2x.jpg">
+                            <picture>
+                                <source srcset="/images/astronauta-2x.webp" type="image/webp" />
+                                <source srcset="/images/astronauta.jpg 1x, /images/astronauta-2x.jpg 2x" type="image/jpeg" />
+                                <img class="rounded" srcset="/images/astronauta.jpg" alt="Da grande voglio fare l'astronauta" />
+                            </picture>
+                        </a>
+                    </figure>
+                </section>
+                <section class="_right sm:float-right sm:w-1/2 sm:pl-1">
+                    <p>An interactive book for children. The tale of a kid who wants to become an astronaut. I developed the iOS part of the project to completion, but sadly the app was never released.</p>
+                    <p><strong>Technologies</strong>: iOS.</p>
+                    <p><a href="https://www.behance.net/gallery/1201947/DA-GRANDE-VOGLIO-FARE-LASTRONAUTA-(-IPAD-APP)"><strong>Behance</strong></a></p>
+                </section>
+            </section>
+            <section class="project full col-start-1 sm:col-span-2 border-t-2 pb-3" id="project_bsc">
+                <section class="_left sm:float-left sm:w-1/2 sm:pr-1">
+                    <h3>CRUSADE Bleeding Score Calculator</h3>
+                </section>
+                <section class="_right sm:float-right sm:w-1/2 sm:pl-2">
+                    <figure>
+                        <a href="#" data-featherlight="/images/bsc-2x.jpg">
+                            <picture>
+                                <source srcset="/images/bsc-2x.webp" type="image/webp" />
+                                <source srcset="/images/bsc.jpg 1x, /images/bsc-2x.jpg 2x" type="image/jpeg" />
+                                <img class="rounded" srcset="/images/bsc.jpg" alt="CRUSADE Bleeding Score Calculator" />
+                            </picture>
+                        </a>
+                    </figure>
+                </section>
+                <section class="_left sm:float-left sm:w-1/2 sm:pr-1">
+                    <p>This app helps clinicians estimate a patient's baseline risk of in-hospital major bleeding during both ST and non-ST segment elevation myocardial infarction. Not that I know what that means. I was given a spreadsheet with some formulas, I was asked to make an iOS app, and so I did. Then the client did not want it anymore, so I published it myself.</p>
+                    <p><strong>Technologies</strong>: iOS (Swift), Android (Kotlin).</p>
+                    <p><a href="https://itunes.apple.com/us/app/crusade-bleeding-score-calculator/id454062627?mt=8"><strong>App Store</strong></a>, <a href="https://play.google.com/store/apps/details?id=net.morpheu5.bleedingscorecalculator"><strong>Google Play</strong></a></p>
+                </section>
+            </section>
+            <section class="project full col-start-1 sm:col-span-2 border-t-2 pb-3" id="project_doodle">
+                <section class="_right sm:float-right sm:w-1/2 sm:pl-1">
+                    <h3>Doodle</h3>
+                </section>
+                <section class="_left sm:float-left sm:w-1/2 sm:pr-2">
+                    <figure>
+                        <a href="#" data-featherlight="/images/doodle-2x.png">
+                            <picture>
+                                <source srcset="/images/doodle-2x.webp" type="image/webp" />
+                                <source srcset="/images/doodle.png 1x, /images/doodle-2x.png 2x" type="image/png" />
+                                <img class="rounded" srcset="/images/doodle.png" alt="Doodle" />
+                            </picture>
+                        </a>
+                    </figure>
+                </section>
+                <section class="_right sm:float-right sm:w-1/2 sm:pl-1">
+                    <p>I worked on the actual Reactable between 2008 and 2009, proposing a new approach to Western tonal music &ndash; the original support was quite minimal, I was tasked with making it a bit more usable.</p>
+                    <p><strong>Technologies</strong>: C++, <a href="https://www.qt.io">Qt 4</a>, <a href="https://www.jackaudio.org">Jack</a>.</p>
+                    <p><a href="https://www.github.com/Morpheu5/Doodle"><strong>Source code</strong></a> | <a href="https://vimeo.com/4325822"><strong>Demo</strong></a></p>
+                </section>
+            </section>
+            <section class="project full col-start-1 sm:col-span-2 border-t-2" id="project_tofus">
+                <section class="_left sm:float-left sm:w-1/2 sm:pr-1">
+                    <h3>tOfuS: the Framework OS</h3>
+                </section>
+                <section class="_right sm:float-right sm:w-1/2 sm:pl-2">
+                    <figure>
+                        <a href="#" data-featherlight="/images/tofus-2x.jpg">
+                            <picture>
+                                <source srcset="/images/tofus-2x.webp" type="image/webp" />
+                                <source srcset="/images/tofus.jpg 1x, /images/tofus-2x.jpg 2x" type="image/jpeg" />
+                                <img class="rounded" srcset="/images/tofus.jpg" alt="tOfuS" />
+                            </picture>
+                        </a>
+                    </figure>
+                </section>
+                <section class="_left sm:float-left sm:w-1/2 sm:pr-1">
+                    <p>Yes, there was a time when I worked on a toy operating system. It was good fun and I learned a lot about assembly code and bare-metal programming.</p>
+                    <p><strong>Technologies</strong>: x86 assembly, C.</p>
+                    <p><a href="https://www.github.com/Morpheu5/tOfuS"><strong>Source code</strong></a></p>
+                </section>
+            </section>
+            <section class="full col-start-1 sm:col-span-2">
+                <p class="calltoaction">Have a cool project in mind? <a href="/contact">Let's talk about it!</a></p>
+            </section>
+        </article>
+    </div>
+</NuxtLayout></template>
+
+<script lang="ts" setup>
+definePageMeta({
+    layout: false
+});
+
+const headline = "Software development";
+const tagline = "Not black magic";
+
+const title = `${headline} ••• Andrea Franceschini, PhD`
+const keywords = "software, development, music, learning, tabletop, research, the open university, university of cambridge, isaac physics, reactable";
+const description = "All the bits and bobs that make computers go 'boop'.";
+
+useState('pageClasses', () => ['development', 'page']);
+useHead({
+    title,
+    meta: [
+        { property: 'keywords', content: keywords },
+        { property: 'DC.Title', content: title },
+        { property: 'twitter:title', content: title },
+        { property: 'og:title', content: title },
+        { property: 'description', content: description },
+        { property: 'DC.Description', content: description },
+        { property: 'twitter:description', content: description },
+        { property: 'og:description', content: description },
+    ]
+});
+</script>

+ 0 - 17
pages/errors/403.php

@@ -1,17 +0,0 @@
-<?php
-$bodyclasses = '403 error page';
-$title = '403';
-$tagline = 'Stop right there';
-$description = 'Yep, you are looking at something you should not be looking at.';
-?>
-
-<?=i('_header')?>
-
-	<article class="body grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-0">
-		<section class="full col-start-1 sm:col-span-2">
-			<p>Sorry, you are looking at something you should not be looking at.</p>
-			<p>I'm afraid I must terminate you.</p>
-		</section>
-	</article>
-		
-<?=i('_footer')?>

+ 0 - 20
pages/errors/404.php

@@ -1,20 +0,0 @@
-<?php
-$bodyclasses = '404 error page';
-$title = '404';
-$tagline = 'Something\'s wrong';
-$description = 'Yep, you are looking for something that is not here.';
-?>
-
-<?=i('_header')?>
-
-	<article class="body grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-0">
-		<section class="full col-start-1 sm:col-span-2">
-			<header>
-				<h2>Nope</h2>
-			</header>
-			<p>Sorry, you are looking for something that is not here.</p>
-			<p>And just in case, I'm not a teapot.</p>
-		</section>
-	</article>
-		
-<?=i('_footer')?>

+ 0 - 37
pages/eumyths.php

@@ -1,37 +0,0 @@
-<?php
-$bodyclasses = 'page';
-$title = 'EU Myths bot';
-$tagline = '';
-$description = 'A bot that debunks brexit lies and fake news';
-$keywords = '';
-$twitter_image = $baseurl.'images/eubot.png';
-?>
-
-<?=i('_header')?>
-
-	<article class="body grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-0">
-		<section class="full col-start-1 sm:col-span-2">
-			<h2>British EU Myths</h2>
-			</h3><strong>A <a href="https://en.wikipedia.org/wiki/Internet_bot">bot</a> that <a href="https://en.wikipedia.org/wiki/Debunker">debunks</a> <a href="https://en.wikipedia.org/wiki/Fake_news">fake news</a></strong></h3>
-			<p class="text-pink-800 font-bold">The bot is currently on hiatus.</p>
-			<p>
-				<a href="https://twitter.com/eumyths"><img src="<?=$baseurl?>images/eubot.png" style="width: 150px; height: 150px; float: right; margin: 0 16px" /></a>
-				The European Commission <a href="https://blogs.ec.europa.eu/ECintheUK/euromyths-a-z-index/">has archived an astonishing 426 lies</a> that the British press told about the European Union between 1992 and 2017.
-				It is widely accepted that <a href="https://www.theguardian.com/commentisfree/2016/jul/15/brexit-boris-johnson-euromyths-telegraph-brussels">this trend was started by Boris Johnson</a>, a man who has a track record that would make Pinocchio blush, while he was posted as the Daily Telegraph's Brussels correspondent between 1989 and 1994, and was happily followed by pretty much every other newspaper in the United Kingom ever since.
-			</p>
-			<p>Boris Johnson became the UK's Prime Minister on the 23<sup>rd</sup> of July 2019, on what <a href="https://www.independent.co.uk/news/uk/politics/brexit-no-deal-adverts-boris-johnson-michael-gove-eu-a9085201.html">it is feared</a> to be a covert operation to leave the European Union without a deal.</p>
-			<p>&nbsp;</p>
-			<p>Anyway, this bot will tweet a timeline of EU myths that are to this day widely believed by the British public.
-			   There will be hourly tweets 9:00-17:00 (London time<sup>*</sup>), every day excluding weekends, because our shiny friends deserve reasonable working hours and rest days too!
-			</p>
-			<p>&nbsp;</p>
-			<p>The bot is operated by <a href="https://andreafranceschini.org">me</a>, and I may occasionally tweet through it, if necessary.</p>
-			<p>I also tweet as <a href="https://twitter.com/morpheu5">@morpheu5</a>. Come say hi. 🙂</p>
-			<p>&nbsp;</p>
-			<hr />
-			<p><sup>*</sup> That's assuming that the UK will not do away with Daylight Savings straight after leaving the EU.</p>
-			<p>&nbsp;</p>
-		</section>
-	</article>
-
-<?=i('_footer')?>

+ 43 - 0
pages/eumyths.vue

@@ -0,0 +1,43 @@
+<template><NuxtLayout name="default">
+    <template #headline>{{ headline }}</template>
+    <template #tagline>{{ tagline }}</template>
+
+    <div class="container mx-auto max-w-screen-lg sm:px-12 px-2 prose">
+        <article class="body grid grid-cols-1 gap-x-12 gap-y-0">
+            <h2>British EU Myths</h2>
+            <h3><strong>A <a href="https://en.wikipedia.org/wiki/Internet_bot">bot</a> that <a href="https://en.wikipedia.org/wiki/Debunker">debunks</a> <a href="https://en.wikipedia.org/wiki/Fake_news">fake news</a></strong></h3>
+            <p class="text-pink-800 font-bold"><strong>The bot is currently on hiatus.</strong></p>
+            <p>
+                <a href="https://twitter.com/eumyths"><img src="/images/eubot.webp" style="width: 150px; height: 150px; float: right; margin: 0 16px" /></a>
+                The European Commission <a href="https://blogs.ec.europa.eu/ECintheUK/euromyths-a-z-index/">has archived an astonishing 426 lies</a> that the British press told about the European Union between 1992 and 2017.
+                It is widely accepted that <a href="https://www.theguardian.com/commentisfree/2016/jul/15/brexit-boris-johnson-euromyths-telegraph-brussels">this trend was started by Boris Johnson</a>, a man who has a track record that would make Pinocchio blush, while he was posted as the Daily Telegraph's Brussels correspondent between 1989 and 1994, and was happily followed by pretty much every other newspaper in the United Kingom ever since.
+            </p>
+            <p>Boris Johnson became the UK's Prime Minister on the 23<sup>rd</sup> of July 2019, on what <a href="https://www.independent.co.uk/news/uk/politics/brexit-no-deal-adverts-boris-johnson-michael-gove-eu-a9085201.html">it is feared</a> to be a covert operation to leave the European Union without a deal**.</p>
+            <p>Anyway, this bot will tweet a timeline of EU myths that are to this day widely believed by the British public.
+               There will be hourly tweets 9:00-17:00 (London time<sup>*</sup>), every day excluding weekends, because our shiny friends deserve reasonable working hours and rest days too!
+            </p>
+            <p>The bot is operated by <a href="https://andreafranceschini.org">me</a>, and I may occasionally tweet through it, if necessary.</p>
+            <p>I also tweet as <a href="https://twitter.com/morpheu5">@morpheu5</a>. Come say hi. 🙂</p>
+            <hr />
+            <p><sup>*</sup> That's assuming that the UK will not do away with Daylight Savings straight after leaving the EU.</p>
+            <p><sup>**</sup> This turned out to be false. However, what happened is much, much worse.</p>
+            <p>&nbsp;</p>
+        </article>
+    </div>
+</NuxtLayout></template>
+
+<script lang="ts" setup>
+definePageMeta({
+    layout: false
+});
+
+const headline = "";
+const tagline = "";
+
+const title = `${headline} ••• Andrea Franceschini, PhD`;
+
+useState('pageClasses', () => ['eumyths', 'page']);
+useHead({
+    title
+});
+</script>

+ 0 - 45
pages/home.php

@@ -1,45 +0,0 @@
-<?php
-$bodyclasses = 'home page';
-
-$description = 'I am a Research Associate at the University of Cambridge, but I also do other things.';
-$keywords = 'music, learning, tabletop, research, the open university, university of cambridge, isaac physics, reactable';
-$twitter_image = 'images/colopic.jpg';
-?>
-
-<?=i('_header')?>
-
-<h2 class="py-2 text-center">I do things.</h2>
-
-<style>
-	.embed-container {
-		position: relative;
-		padding-bottom: 56.25%;
-		height: 0;
-		overflow: hidden;
-		max-width: 100%;
-	}
-
-	.embed-container iframe,
-	.embed-container object,
-	.embed-container embed {
-		position: absolute;
-		top: 0;
-		left: 0;
-		width: 100%;
-		height: 100%;
-	}
-</style>
-<div class='embed-container -mb-8'>
-	<iframe loading="lazy" src='https://www.youtube.com/embed/videoseries?list=PL70paWUyPRgqhqxG0ShlROn1eSHlekhB_' frameborder='0' allowfullscreen></iframe>
-</div>
-
-<h2 class="py-2 text-center">What can I do for you?</h2>
-
-<div id="home_bars" class="flex flex-row">
-	<a href="<?=$baseurl?>research" class="research flex-1"><span>Research</span></a>
-	<a href="<?=$baseurl?>development" class="development flex-1"><span>Development</span></a>
-	<a href="<?=$baseurl?>writing" class="writing flex-1"><span>Technical Writing</span></a>
-	<a href="<?=$baseurl?>contact" class="other flex-1"><span>Something else</span></a>
-</div>
-
-<?=i('_footer')?>

+ 139 - 0
pages/index.vue

@@ -0,0 +1,139 @@
+<template><NuxtLayout name="default">
+    <div class="container mx-auto max-w-screen-xl md:px-12 sm:px-0 px-0">
+        <h3 class="sm:text-5xl text-3xl text-center -mt-6 sm:-mt-16">
+            I do things.
+        </h3>
+        <div class="sm:max-w-screen-lg mx-auto sm:py-8 py-6">
+            <div class="aspect-w-16 aspect-h-9">
+                <iframe ref="video" loading="lazy" data-src="https://www.youtube-nocookie.com/embed/AszBiqrvAaU" title="How to make your computer go BOOP" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
+            </div>
+        </div>
+        <h3 class="sm:text-4xl text-3xl pb-8 text-center">
+            <span class="scroll-down sm:hidden">
+                <ScrollDownSVG />
+            </span>
+            What can I do for you?
+            <span class="scroll-down sm:hidden">
+                <ScrollDownSVG />
+            </span>
+        </h3>
+        <div id="home_bars" class="flex flex-row sm:max-w-screen-lg mx-auto">
+            <a href="/research" class="research flex-1"><span>Research</span></a>
+            <a href="/development" class="development flex-1"><span>Development</span></a>
+            <a href="/writing" class="writing flex-1"><span>Technical Writing</span></a>
+            <a href="/contact" class="other flex-1"><span>Something else</span></a>
+        </div>
+    </div>
+</NuxtLayout></template>
+
+<style lang="pcss">
+@keyframes floating {
+    0% { transform: translate(0,  -5px); }
+    50%   { transform: translate(0, 0px); }   
+    100%   { transform: translate(0, -5px); }   
+}
+
+.scroll-down {
+    svg {
+        width: 25px;
+        height: 25px;
+        display: inline;
+        animation-name: floating;
+        animation-duration: 2s;
+        animation-iteration-count: infinite;
+        animation-timing-function: ease-in-out;
+        opacity: 50%;
+    }
+}
+
+html.no-webp {
+    #home_bars a {
+        &.research {
+            background-image: url(../images/home-research-2x.png);
+        }
+        &.development {
+            background-image: url(../images/home-development-2x.png);
+        }
+        &.writing {
+            background-image: url(../images/home-writing-2x.png);
+        }
+        &.other {
+            background-image: url(../images/home-contact-2x.png);
+        }
+    }
+}
+#home_bars {
+    a {
+        width: 25%;
+        height: 250px;
+        padding: 0;
+        margin: 0;
+        position: relative;
+        background: grey;
+
+        &.research {
+            background-image: url(../images/home-research-2x.webp);
+            background-position: 100% 100%;
+            background-size: cover;
+        }
+        &.development {
+            background-image: url(../images/home-development-2x.webp);
+            background-position: 100% 100%;
+            background-size: cover;
+        }
+        &.writing {
+            background-image: url(../images/home-writing-2x.webp);
+            background-position: 100% 100%;
+            background-size: cover;
+        }
+        &.other {
+            background-image: url(../images/home-contact-2x.webp);
+            background-position: 100% 100%;
+            background-size: cover;
+        }
+
+        span {
+            position: absolute;
+            color: #fff;
+            font-family: Oswald,sans-serif;
+            font-size: 30px;
+            white-space: nowrap;
+            transform-origin: bottom left;
+            transform: rotate(-90deg);
+            display: block;
+            bottom: 0.5em;
+            left: 1.6667em;
+            overflow: visible;
+        }
+    }
+}
+</style>
+
+<script lang="ts" setup>
+definePageMeta({
+    layout: false
+});
+const title = "Hello, I'm Andrea Franceschini, PhD";
+const keywords = "music, learning, tabletop, research, the open university, university of cambridge, isaac physics, reactable";
+
+useState('pageClasses', () => ['index', 'page']);
+useState('headline', () => "Hello");
+useState('tagline', () => "I'm Andrea");
+useHead({
+    title,
+    meta: [
+        { property: 'keywords', content: keywords },
+        { property: 'DC.Title', content: title },
+        { property: 'twitter:title', content: title },
+        { property: 'og:title', content: title },
+    ]
+});
+
+const video = ref<HTMLIFrameElement>();
+
+onMounted(() => {
+    // @ts-ignore
+    const src = video.value?.dataset['src'];
+    video.value?.setAttribute('src', src || '');
+});
+</script>

+ 27 - 21
pages/qualia.php → pages/qualia.vue

@@ -1,17 +1,10 @@
-<?php
-$bodyclasses = 'page';
-$title = 'Qualia Computing';
-$tagline = '';
-$description = 'A dossier on Qualia Computing';
-$keywords = '';
-$twitter_image = '';
-?>
+<template><NuxtLayout name="default">
+    <template #headline>{{ headline }}</template>
+    <template #tagline>{{ tagline }}</template>
 
-<?=i('_header')?>
-
-    <article class="body grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-0">
-		<section class="full col-start-1 sm:col-span-2">
-			<h2>Qualia Computing</h2>
+    <div class="container mx-auto max-w-screen-lg sm:px-12 px-2 prose">
+        <article class="body grid grid-cols-1 gap-x-12 gap-y-0">
+            <h2>Qualia Computing</h2>
             <p>On the 13<sup>th</sup> of May 2020, I woke up to find an email from <a href="https://www.linkedin.com/in/nicholas-rayner-2251903a/?originalSubdomain=uk">Nicholas Rayner</a> about this project of his, Qualia Computing.
                 I was copied with a Professor I had collaborated with in the past, and the text referenced subjects of our past collaboration, so I assumed this was a student asking for a final year project for their undergraduate degree.</p>
             <blockquote><p>I am Nicholas Rayner, i'm a former Ravenbourne College Interaction Design student.
@@ -32,14 +25,27 @@ $twitter_image = '';
                 when I began thinking it wasn't my morning slumber preventing
                 me from making sense of the email: it was in fact the email that
                 wasn't making any sense at all.</p>
-            <p>&raquo; <a href="<?=$baseurl?>/files/qualia/Qualia Computing email 01.pdf">Read the first message</a>.</p>
+            <p>&raquo; <a href="/files/qualia/Qualia Computing email 01.pdf">Read the first message</a>.</p>
             <p>I sent the message around to some friends as an amusement and then forgot about it. Until the 29th of May 2020.</p>
-            <p>&nbsp;</p>
-            <p>Nicholas, prophet of God, is back.</p>
-            <p>&raquo; <a href="<?=$baseurl?>/files/qualia/Qualia Computing email 02.pdf">Read the second message</a>.</p>
-            <p>&nbsp;</p>
+            <h3>Nicholas, prophet of God, is back.</h3>
+            <p>&raquo; <a href="/files/qualia/Qualia Computing email 02.pdf">Read the second message</a>.</p>
             <p>The saga continues.<br />Last updated: 2020-05-29 10:00 BST.</p>
-		</section>
-	</article>
+        </article>
+    </div>
+</NuxtLayout></template>
+
+<script lang="ts" setup>
+definePageMeta({
+    layout: false
+});
+
+const headline = "";
+const tagline = "";
+
+const title = `${headline} ••• Andrea Franceschini, PhD`;
 
-<?=i('_footer')?>
+useState('pageClasses', () => ['qualia', 'page']);
+useHead({
+    title
+});
+</script>

+ 0 - 50
pages/research.php

@@ -1,50 +0,0 @@
-<?php
-$bodyclasses = 'research page';
-$title = 'Research';
-$tagline = 'not a synonym for “google”';
-$description = 'I do research. Actual research.';
-$keywords = 'music, learning, tabletop, research, the open university, university of cambridge, isaac physcs, reactable';
-$twitter_image = 'images/table_2-2x.jpg';
-$banner_image = 'images/banner_research-2x.jpg';
-$banner_image_half = 'images/banner_research.jpg';
-?>
-
-<?=i('_header')?>
-
-	<article class="body grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-0">
-		<section class="full col-start-1 sm:col-span-2 -pb-8">
-			<p>I am a trained computer scientist and I have done qualitative research. This means I'm comfortable with analysing numbers and words alike.</p>
-		</section>
-		<section class="left col-start-1 col-span-1">
-			<figure>
-				<picture>
-					<source srcset="<?=$baseurl?>images/reactable.jpg 1x, <?=$baseurl?>images/reactable-2x.jpg 2x" />
-					<img class="rounded" srcset="<?=$baseurl?>images/reactable.jpg" alt="Reactable" />
-				</picture>
-				<figcaption><span>An interactive musical tabletop.</span></figcaption>
-			</figure>
-			<p>When I started my doctoral studies, there was an alarming lack of empirical studies in the area of <strong>interactive tabletops</strong> as <strong>music learning tools</strong>. It is not clear what happens when you give people a giant touch screen, and teach them music with it. This means you have to sit down and watch them, record their actions, and analyse their conversations.</p>
-		</section>
-		<section class="right col-start-1 col-span-1 sm:col-start-2">
-			<h2>Qualitative research</h2>
-			<p>Qualitative research is hard and fascinating work: it gives you insight on what people do and think &ndash; like reading minds, if you think about it.</p>
-			<p>QR is at the heart of <strong>user experience research</strong>, among other applications &ndash; that means figuring out what people think about the things you make, and how they use them.</p>
-			<p>You can look at pretty much anything for information. Typical data sources include interviews, journals, videos, and so on. However, it is a common misconception that qualitative findings are less rigorous than quantitative findings: they are simply different, and they can even work together to strengthen each other and produce even more rigorous research.</p>
-		</section>
-		<section class="full col-start-1 sm:col-span-2 border-t-2">
-			<h2>What can I help you with?</h2>
-			<p>Do you have a pressing question that needs answer? Do you want to make your app better and easier to use? Do you have a brilliant new idea and want to know if it's worth developing it?</p>
-			<p>Things I <strong>can</strong> help you with include</p>
-			<ul>
-				<li><strong>User research</strong> (ethnographies, surveys, focus groups, A/B testing…)</li>
-				<li><strong>Interaction design</strong> (UX, prototyping, behaviour, patterns, iterative design…)</li>
-				<li><strong>Usability</strong> (web sites, graphical and tangible interfaces…)</li>
-			</ul>
-			<p>I can work with your designers and developers to help you look into ideas and products, prototype and evaluate them, and finally build and release amazing solutions.</p>
-		</section>
-		<section class="full col-start-1 sm:col-span-2">
-			<p class="calltoaction">Curious? <a href="<?=$baseurl?>contact">Get in touch!</a></p>
-		</section>
-	</article>
-
-<?=i('_footer')?>

+ 72 - 0
pages/research.vue

@@ -0,0 +1,72 @@
+<template><NuxtLayout name="default">
+    <template #headline>{{ headline }}</template>
+    <template #tagline>{{ tagline }}</template>
+
+    <div class="container mx-auto max-w-screen-lg sm:px-12 px-2 prose">
+        <article class="body grid sm:grid-cols-2 grid-cols-1 gap-x-12 gap-y-0">
+            <section class="sm:col-span-2">
+                <p>I am a trained computer scientist and I have done qualitative research. This means I'm comfortable with analysing numbers and words alike.</p>
+            </section>
+            <section class="left col-start-1 col-span-1">
+                <figure>
+                    <picture>
+                        <source srcset="/images/reactable-2x.webp" type="image/webp" />
+                        <source srcset="/images/reactable.jpg 1x, /images/reactable-2x.jpg 2x" />
+                        <img class="rounded" srcset="/images/reactable.jpg" alt="Reactable" />
+                    </picture>
+                    <figcaption><span>An interactive musical tabletop.</span></figcaption>
+                </figure>
+                <p>When I started my doctoral studies, there was an alarming lack of empirical studies in the area of <strong>interactive tabletops</strong> as <strong>music learning tools</strong>. It is not clear what happens when you give people a giant touch screen, and teach them music with it. This means you have to sit down and watch them, record their actions, and analyse their conversations.</p>
+            </section>
+            <section class="right sm:col-start-2">
+                <h2>Qualitative research</h2>
+                <p>Qualitative research is hard and fascinating work: it gives you insight on what people do and think &ndash; like reading minds, if you think about it.</p>
+                <p>QR is at the heart of <strong>user experience research</strong>, among other applications &ndash; that means figuring out what people think about the things you make, and how they use them.</p>
+                <p>You can look at pretty much anything for information. Typical data sources include interviews, journals, videos, and so on. However, it is a common misconception that qualitative findings are less rigorous than quantitative findings: they are simply different, and they can even work together to strengthen each other and produce even more rigorous research.</p>
+            </section>
+            <section class="full sm:col-span-2 pt-6 border-t-2">
+                <h2>What can I help you with?</h2>
+                <p>Do you have a pressing question that needs answer? Do you want to make your app better and easier to use? Do you have a brilliant new idea and want to know if it's worth developing it?</p>
+                <p>Things I <strong>can</strong> help you with include</p>
+                <ul>
+                    <li><strong>User research</strong> (ethnographies, surveys, focus groups, A/B testing…)</li>
+                    <li><strong>Interaction design</strong> (UX, prototyping, behaviour, patterns, iterative design…)</li>
+                    <li><strong>Usability</strong> (web sites, graphical and tangible interfaces…)</li>
+                </ul>
+                <p>I can work with your designers and developers to help you look into ideas and products, prototype and evaluate them, and finally build and release amazing solutions.</p>
+            </section>
+            <section class="full col-start-1 sm:col-span-2">
+                <p class="calltoaction">Curious? <a href="/contact">Get in touch!</a></p>
+            </section>
+        </article>
+    </div>
+</NuxtLayout></template>
+
+<script lang="ts" setup>
+definePageMeta({
+    layout: false
+});
+
+const headline = "Research";
+const tagline = 'Not the same as "google"';
+
+const title = `${headline} ••• Andrea Franceschini, PhD`;
+const keywords = "research, science, qualitative, quantitative, experiments, music, learning, tabletop, research, the open university, university of cambridge, isaac physics, reactable";
+const description = "Science!";
+
+useState('top-banner-image', () => '/images/banner_research-2x.webp');
+useState('pageClasses', () => ['research', 'page']);
+useHead({
+    title,
+    meta: [
+        { property: 'keywords', content: keywords },
+        { property: 'DC.Title', content: title },
+        { property: 'twitter:title', content: title },
+        { property: 'og:title', content: title },
+        { property: 'description', content: description },
+        { property: 'DC.Description', content: description },
+        { property: 'twitter:description', content: description },
+        { property: 'og:description', content: description },
+    ]
+});
+</script>

+ 0 - 57
pages/writing.php

@@ -1,57 +0,0 @@
-<?php
-$bodyclasses = 'writing page';
-$title = 'Technical writing';
-$tagline = 'tutorials, manuals, reports…';
-$description = 'I do technical writing, revisions, translations, and such.';
-$keywords = 'music, learning, tabletop, research, technical, writing';
-$twitter_image = 'images/colopic.jpg';
-$banner_image = 'images/banner_writing-2x.jpg';
-$banner_image_half = 'images/banner_writing.jpg';
-?>
-
-<?=i('_header')?>
-
-	<article class="body grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-0">
-		<section class="full col-start-1 sm:col-span-2">
-			<p>Did you know that researchers spend about half of their professional lives doing academic and technical writing? For example, research students spend at least a third of their studies writing their theses, without counting papers, and bids for grants.</p>
-			<p>Technical writing includes a lot of different types of texts, such as scientific papers, technical reports, manuals, tutorials, and so on.</p>
-			<h2>Things I have done</h2>
-			<p>I have some experience in writing technical reports and tutorials, and my refereed publications are evidence that I can write at an academic level. Here are some examples.</p>
-			<h3>Tutorials</h3>
-			<ul class="nomargin">
-				<li>Series of “<a href="//blog.morpheu5.net/categoria/learn-to-code/page/2/">Learn to code</a>” posts on my personal blog (in Italian)</li>
-				<li>Series on Drupal for the Italian magazine <a href="//www.oltrelinux.com">Linux&amp;C</a>, in issues <a href="<?=$baseurl?>files/LC73_drupal.pdf">73</a>, <a href="<?=$baseurl?>files/LC74_drupal.pdf">74</a>, <a href="<?=$baseurl?>files/LC75_drupal.pdf">75</a></li>
-				<li>Series on Inkscape for the Italian magazine <a href="//www.oltrelinux.com">Linux&amp;C</a>, in issues <a href="<?=$baseurl?>files/LC64_inkscape.pdf">64</a>, <a href="<?=$baseurl?>files/LC67_inkscape.pdf">67</a></li>
-			</ul>
-			<h3>Other (more or less) technical writing</h3>
-			<ul class="nomargin">
-				<li><a href="https://hackernoon.com/the-vizcoin-revolution-8a6f12fc5df6">The #VIZcoin revolution</a> (hackernoon.com)</li>
-				<li><a href="https://hackernoon.com/kotlin-is-cheating-on-me-e048cde4f66">Kotlin is cheating on me…</a> (hackernoon.com)</li>
-				<li><a href="https://hackernoon.com/android-is-hard-b7a5a5549655">Android is hard</a> (hackernoon.com)</li>
-				<li><a href="https://hackernoon.com/how-i-messed-up-my-artsy-apps-launch-e684111de532">How I messed up my artsy app’s launch</a> (hackernoon.com)</li>
-				<li><a href="https://medium.com/@morpheu5/how-i-made-an-artsy-app-based-in-science-a3690164a9ce">How I made an artsy app based on science</a> (medium.com)</li>
-			</ul>
-			<h3>Refereed publications</h3>
-			<ul class="nomargin">
-				<li>A. Franceschini, R. Laney, C. Dobbyn (2022) <a href="" title="This article is not published yet. A download link will be available soon after publication.">Sketching Music together: mixed groups exploring melodic similarity and contrast using a Digital Tabletop</a>. In <em>Journal of Music, Technology &amp; Education<!--, Volume 1x, Number y--></em> (Accepted for publication). <!-- (<a href="">Scopus</a>) --></li>
-				<li>T. Battistin, N. Dalla Pozza, S. Trentin, G. Volpin, A. Franceschini, RHF therapists team, A. Rodà (2022), <a href="" title="This article is not published yet. A download link will be available soon after publication.">Co-designed mini-games for children with visual impairment: a pilot study on their usability</a>. In <em>Multimedia Tools and Applications</em> (Accepted for publication).</li>
-				<li>J. Waite, A. Franceschini, S. Sentance, M. Patterson, J. Sharkey (2021), <a href="https://doi.org/10.1145/3481282.3481287">An online platform for teaching Upper Secondary school Computer Science</a>. In <em>Proceedings of the United Kingdom and Ireland Computing Education Research (UKICER 2021) Conference</em>. (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-85115215700&origin=resultslist">Scopus</a>)</li>
-				<li>A. Franceschini, R. Laney, C. Dobbyn (2020) <a href="https://doi.org/10.1386/jmte_00014_1">Sketching Music: Exploring melodic similarity and contrast using a Digital Tabletop</a>. In <em>Journal of Music, Technology &amp; Education, Volume 13, Number 1</em> (August 2020). (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-85099455819&origin=resultslist">Scopus</a>)</li>
-				<li>A. Franceschini, J. P. Sharkey, A. R. Beresford (2020) <a href="<?=$baseurl?>files/ccers20-inequality-poster-combo.pdf" title="PDF, 1.5 MB">Effective use of mathematical equations in an online learning environment</a>. In <em>Proceedings of the Cambridge Computing Education Research Symposium 2020</em>, Cambridge, UK.</li>
-				<li>A. Franceschini, J. P. Sharkey, A. R. Beresford (2019) <a href="<?=$baseurl?>files/las2019.pdf" title="PDF, 2 MB">Inequality: multi-modal equation entry on the web</a>. In <em>Proceedings of Learning @ Scale 2019</em>, Chicago, IL, USA. (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-85083953064&origin=resultslist">Scopus</a>)</li>
-				<li>A. Franceschini, R. Laney, C. Dobbyn (2016) <a href="<?=$baseurl?>files/met2016.pdf" title="PDF, 87 KB">Sketching music: making music through exploring art</a>. In <em>Proceedings of Sempre: Music, Education, Technology (MET2016)</em>, London, UK.</li>
-				<li>A. Franceschini, R. Laney, C. Dobbyn (2014) <a href="<?=$baseurl?>files/icmcsmc2014.pdf" title="PDF, 854 KB">Learning Musical Contour on a Tabletop</a>. In <em>Proceedings of the 40th International Computer Music Conference and 11th Sound and Music Computing Conference (ICMC-SMC)</em>, Athens, Greece. (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-84908884791&origin=resultslist">Scopus</a>)</li>
-				<li>A. Franceschini (2010) <a href="<?=$baseurl?>files/smc2010.pdf" title="PDF, 570 KB">Towards a practical approach to music theory on the Reactable</a>. In <em>Proceedings of the 7th Sound and Music Computing Conference (SMC)</em>, Barcelona, Spain. (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-84905169162&origin=resultslist">Scopus</a>)</li>
-			</ul>
-			<h3>Other academic works</h3>
-			<ul class="nomargin">
-				<li>PhD thesis: <a href="<?=$baseurl?>files/phd-thesis.pdf" title="PDF, 8.1 MB">Learning to use melodic similarity and contrast for narrative using a Digital Tabletop Musical Interface</a></li>
-				<li>Master thesis: <a href="<?=$baseurl?>files/master_thesis.pdf">A practical approach to Music Theory on the Reactable</a></li>
-			</ul>
-		</section>
-		<section class="full col-start-1 sm:col-span-2">
-			<p class="calltoaction">Need something written? <a href="<?=$baseurl?>contact">Get in touch!</a></p>
-		</section>
-	</article>
-
-<?=i('_footer')?>

+ 77 - 0
pages/writing.vue

@@ -0,0 +1,77 @@
+<template><NuxtLayout name="default">
+    <template #headline>{{ headline }}</template>
+    <template #tagline>{{ tagline }}</template>
+
+    <div class="container mx-auto max-w-screen-lg sm:px-12 px-2 prose">
+        <article class="body grid sm:grid-cols-2 grid-cols-1 gap-x-12 gap-y-0">
+            <section class="full sm:col-span-2">
+                <p>Did you know that researchers spend about half of their professional lives doing academic and technical writing? For example, research students spend at least a third of their studies writing their theses, without counting papers, and bids for grants.</p>
+                <p>Technical writing includes a lot of different types of texts, such as scientific papers, technical reports, manuals, tutorials, and so on.</p>
+                <h2>Things I have done</h2>
+                <p>I have some experience in writing technical reports and tutorials, and my refereed publications are evidence that I can write at an academic level. Here are some examples.</p>
+                <h3>Tutorials</h3>
+                <ul class="nomargin">
+                    <li>Series of “<a href="https://blog.morpheu5.net/categoria/learn-to-code/page/2/">Learn to code</a>” posts on my personal blog (in Italian)</li>
+                    <li>Series on Drupal for the Italian magazine <a href="https://www.oltrelinux.com">Linux&amp;C</a>, in issues <a href="/files/LC73_drupal.pdf">73</a>, <a href="/files/LC74_drupal.pdf">74</a>, <a href="/files/LC75_drupal.pdf">75</a></li>
+                    <li>Series on Inkscape for the Italian magazine <a href="https://www.oltrelinux.com">Linux&amp;C</a>, in issues <a href="/files/LC64_inkscape.pdf">64</a>, <a href="/files/LC67_inkscape.pdf">67</a></li>
+                </ul>
+                <h3>Other (more or less) technical writing</h3>
+                <ul class="nomargin">
+                    <li><a href="https://hackernoon.com/the-vizcoin-revolution-8a6f12fc5df6">The #VIZcoin revolution</a> (hackernoon.com)</li>
+                    <li><a href="https://hackernoon.com/kotlin-is-cheating-on-me-e048cde4f66">Kotlin is cheating on me…</a> (hackernoon.com)</li>
+                    <li><a href="https://hackernoon.com/android-is-hard-b7a5a5549655">Android is hard</a> (hackernoon.com)</li>
+                    <li><a href="https://hackernoon.com/how-i-messed-up-my-artsy-apps-launch-e684111de532">How I messed up my artsy app’s launch</a> (hackernoon.com)</li>
+                    <li><a href="https://medium.com/@morpheu5/how-i-made-an-artsy-app-based-in-science-a3690164a9ce">How I made an artsy app based on science</a> (medium.com)</li>
+                </ul>
+                <h3>Refereed publications</h3>
+                <ul class="nomargin">
+                    <li>(Accepted for publication) <span class="opacity-60">A. Franceschini, R. Laney, C. Dobbyn (2022) <a href="" title="This article is not published yet. A download link will be available soon after publication.">Sketching Music together: mixed groups exploring melodic similarity and contrast using a Digital Tabletop</a>. In <em>Journal of Music, Technology &amp; Education<!--, Volume 1x, Number y--></em>. <!-- (<a href="">Scopus</a>) --></span></li>
+                    <li>(Accepted for publication) <span class="opacity-60">T. Battistin, N. Dalla Pozza, S. Trentin, G. Volpin, A. Franceschini, RHF therapists team, A. Rodà (2022), <a href="" title="This article is not published yet. A download link will be available soon after publication.">Co-designed mini-games for children with visual impairment: a pilot study on their usability</a>. In <em>Multimedia Tools and Applications</em>.</span></li>
+                    <li>J. Waite, A. Franceschini, S. Sentance, M. Patterson, J. Sharkey (2021), <a href="https://doi.org/10.1145/3481282.3481287">An online platform for teaching Upper Secondary school Computer Science</a>. In <em>Proceedings of the United Kingdom and Ireland Computing Education Research (UKICER 2021) Conference</em>. (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-85115215700&origin=resultslist">Scopus</a>)</li>
+                    <li>A. Franceschini, R. Laney, C. Dobbyn (2020) <a href="https://doi.org/10.1386/jmte_00014_1">Sketching Music: Exploring melodic similarity and contrast using a Digital Tabletop</a>. In <em>Journal of Music, Technology &amp; Education, Volume 13, Number 1</em> (August 2020). (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-85099455819&origin=resultslist">Scopus</a>)</li>
+                    <li>A. Franceschini, J. P. Sharkey, A. R. Beresford (2020) <a href="/files/ccers20-inequality-poster-combo.pdf" title="PDF, 1.5 MB">Effective use of mathematical equations in an online learning environment</a>. In <em>Proceedings of the Cambridge Computing Education Research Symposium 2020</em>, Cambridge, UK.</li>
+                    <li>A. Franceschini, J. P. Sharkey, A. R. Beresford (2019) <a href="/files/las2019.pdf" title="PDF, 2 MB">Inequality: multi-modal equation entry on the web</a>. In <em>Proceedings of Learning @ Scale 2019</em>, Chicago, IL, USA. (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-85083953064&origin=resultslist">Scopus</a>)</li>
+                    <li>A. Franceschini, R. Laney, C. Dobbyn (2016) <a href="/files/met2016.pdf" title="PDF, 87 KB">Sketching music: making music through exploring art</a>. In <em>Proceedings of Sempre: Music, Education, Technology (MET2016)</em>, London, UK.</li>
+                    <li>A. Franceschini, R. Laney, C. Dobbyn (2014) <a href="/files/icmcsmc2014.pdf" title="PDF, 854 KB">Learning Musical Contour on a Tabletop</a>. In <em>Proceedings of the 40th International Computer Music Conference and 11th Sound and Music Computing Conference (ICMC-SMC)</em>, Athens, Greece. (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-84908884791&origin=resultslist">Scopus</a>)</li>
+                    <li>A. Franceschini (2010) <a href="/files/smc2010.pdf" title="PDF, 570 KB">Towards a practical approach to music theory on the Reactable</a>. In <em>Proceedings of the 7th Sound and Music Computing Conference (SMC)</em>, Barcelona, Spain. (<a href="https://www.scopus.com/record/display.uri?eid=2-s2.0-84905169162&origin=resultslist">Scopus</a>)</li>
+                </ul>
+                <h3>Other academic works</h3>
+                <ul class="nomargin">
+                    <li>PhD thesis: <a href="/files/phd-thesis.pdf" title="PDF, 8.1 MB">Learning to use melodic similarity and contrast for narrative using a Digital Tabletop Musical Interface</a></li>
+                    <li>Master thesis: <a href="/files/master_thesis.pdf">A practical approach to Music Theory on the Reactable</a></li>
+                </ul>
+            </section>
+            <section class="full col-start-1 sm:col-span-2">
+                <p class="calltoaction">Need something written? <a href="/contact">Get in touch!</a></p>
+            </section>
+        </article>
+    </div>
+</NuxtLayout></template>
+
+<script lang="ts" setup>
+definePageMeta({
+    layout: false
+})
+
+const headline = "Technical writing";
+const tagline = "Tutorials, manuals, reports…";
+
+const title = `${headline} ••• Andrea Franceschini, PhD`;
+const keywords = "technical, writing, manuals, documentation, tutorials, reports, articles, papers, revision, translation, music, learning, tabletop, research, the open university, university of cambridge, isaac physics, reactable";
+const description = "I do technical writing, revisions, translations, and such.";
+
+useState('pageClasses', () => ['writing', 'page']);
+useHead({
+    title,
+    meta: [
+        { property: 'keywords', content: keywords },
+        { property: 'DC.Title', content: title },
+        { property: 'twitter:title', content: title },
+        { property: 'og:title', content: title },
+        { property: 'description', content: description },
+        { property: 'DC.Description', content: description },
+        { property: 'twitter:description', content: description },
+        { property: 'og:description', content: description },
+    ]
+});
+</script>

+ 21 - 0
plugins/fontawesome.js

@@ -0,0 +1,21 @@
+import { library, config } from '@fortawesome/fontawesome-svg-core'
+import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
+import { faEnvelope, faBlog, faCookieBite, faCircleXmark, faLink } from '@fortawesome/free-solid-svg-icons';
+import { faCircleQuestion, faCopyright } from '@fortawesome/free-regular-svg-icons';
+import { faOrcid, faLinkedin, faGithub, faAppStoreIos, faGooglePlay, faTwitter, faInstagram, faFlickr, faSoundcloud, faYoutube, faDeviantart } from '@fortawesome/free-brands-svg-icons'
+
+// This is important, we are going to let Nuxt worry about the CSS
+config.autoAddCss = false;
+
+// You can add your icons directly in this plugin. See other examples for how you
+// can add other styles or just individual icons.
+
+library.add(
+  faEnvelope, faBlog, faCookieBite, faCircleXmark, faLink,
+  faCircleQuestion, faCopyright,
+  faOrcid, faLinkedin, faGithub, faAppStoreIos, faGooglePlay, faTwitter, faInstagram, faFlickr, faSoundcloud, faYoutube, faDeviantart
+);
+
+export default defineNuxtPlugin((nuxtApp) => {
+  nuxtApp.vueApp.component('fa-icon', FontAwesomeIcon);
+})

+ 27 - 0
plugins/matomo.client.ts

@@ -0,0 +1,27 @@
+// <noscript><p><img src="https://matomo.morpheu5.net/matomo.php?idsite=1&amp;rec=1" style="border:0;" alt="" /></p></noscript>
+
+import { ConsentStore } from "~~/types";
+
+export default defineNuxtPlugin(_nuxtApp => {
+    const consentStore = useCookie('consent-store').value as ConsentStore;
+    const analytics = consentStore?.analytics;
+
+    if (analytics) {
+        const u = "https://matomo.morpheu5.net/";
+        const _paq = window._paq = window._paq || [];
+        _paq.push(['trackPageView']);
+        _paq.push(['enableLinkTracking']);
+        _paq.push(['setTrackerUrl', `${u}matomo.php`]);
+        _paq.push(['setSiteId', process.dev ? '3' : '1']);
+        
+        useHead({
+            script: [
+                {
+                    src: `${u}matomo.js`,
+                    body: true,
+                    async: true,
+                }
+            ]
+        });
+    }
+});

+ 593 - 0
plugins/modernizr.client.js

@@ -0,0 +1,593 @@
+export default defineNuxtPlugin(nuxtApp => {
+  /*!
+  * modernizr v3.6.0
+  * Build https://modernizr.com/download?-webp-setclasses-dontmin
+  *
+  * Copyright (c)
+  *  Faruk Ates
+  *  Paul Irish
+  *  Alex Sexton
+  *  Ryan Seddon
+  *  Patrick Kettner
+  *  Stu Cox
+  *  Richard Herrera
+
+  * MIT License
+  */
+
+  /*
+  * Modernizr tests which native CSS3 and HTML5 features are available in the
+  * current UA and makes the results available to you in two ways: as properties on
+  * a global `Modernizr` object, and as classes on the `<html>` element. This
+  * information allows you to progressively enhance your pages with a granular level
+  * of control over the experience.
+  */
+
+  ;(function(window, document, undefined){
+    var classes = [];
+    
+
+    var tests = [];
+    
+
+    /**
+     *
+     * ModernizrProto is the constructor for Modernizr
+     *
+     * @class
+     * @access public
+     */
+
+    var ModernizrProto = {
+      // The current version, dummy
+      _version: '3.6.0',
+
+      // Any settings that don't work as separate modules
+      // can go in here as configuration.
+      _config: {
+        'classPrefix': '',
+        'enableClasses': true,
+        'enableJSClass': true,
+        'usePrefixes': true
+      },
+
+      // Queue of tests
+      _q: [],
+
+      // Stub these for people who are listening
+      on: function(test, cb) {
+        // I don't really think people should do this, but we can
+        // safe guard it a bit.
+        // -- NOTE:: this gets WAY overridden in src/addTest for actual async tests.
+        // This is in case people listen to synchronous tests. I would leave it out,
+        // but the code to *disallow* sync tests in the real version of this
+        // function is actually larger than this.
+        var self = this;
+        setTimeout(function() {
+          cb(self[test]);
+        }, 0);
+      },
+
+      addTest: function(name, fn, options) {
+        tests.push({name: name, fn: fn, options: options});
+      },
+
+      addAsyncTest: function(fn) {
+        tests.push({name: null, fn: fn});
+      }
+    };
+
+    
+
+    // Fake some of Object.create so we can force non test results to be non "own" properties.
+    var Modernizr = function() {};
+    Modernizr.prototype = ModernizrProto;
+
+    // Leak modernizr globally when you `require` it rather than force it here.
+    // Overwrite name so constructor name is nicer :D
+    Modernizr = new Modernizr();
+
+    
+
+    /**
+     * docElement is a convenience wrapper to grab the root element of the document
+     *
+     * @access private
+     * @returns {HTMLElement|SVGElement} The root element of the document
+     */
+
+    var docElement = document.documentElement;
+    
+
+    /**
+     * is returns a boolean if the typeof an obj is exactly type.
+     *
+     * @access private
+     * @function is
+     * @param {*} obj - A thing we want to check the type of
+     * @param {string} type - A string to compare the typeof against
+     * @returns {boolean}
+     */
+
+    function is(obj, type) {
+      return typeof obj === type;
+    }
+    ;
+
+    /**
+     * Run through all tests and detect their support in the current UA.
+     *
+     * @access private
+     */
+
+    function testRunner() {
+      var featureNames;
+      var feature;
+      var aliasIdx;
+      var result;
+      var nameIdx;
+      var featureName;
+      var featureNameSplit;
+
+      for (var featureIdx in tests) {
+        if (tests.hasOwnProperty(featureIdx)) {
+          featureNames = [];
+          feature = tests[featureIdx];
+          // run the test, throw the return value into the Modernizr,
+          // then based on that boolean, define an appropriate className
+          // and push it into an array of classes we'll join later.
+          //
+          // If there is no name, it's an 'async' test that is run,
+          // but not directly added to the object. That should
+          // be done with a post-run addTest call.
+          if (feature.name) {
+            featureNames.push(feature.name.toLowerCase());
+
+            if (feature.options && feature.options.aliases && feature.options.aliases.length) {
+              // Add all the aliases into the names list
+              for (aliasIdx = 0; aliasIdx < feature.options.aliases.length; aliasIdx++) {
+                featureNames.push(feature.options.aliases[aliasIdx].toLowerCase());
+              }
+            }
+          }
+
+          // Run the test, or use the raw value if it's not a function
+          result = is(feature.fn, 'function') ? feature.fn() : feature.fn;
+
+
+          // Set each of the names on the Modernizr object
+          for (nameIdx = 0; nameIdx < featureNames.length; nameIdx++) {
+            featureName = featureNames[nameIdx];
+            // Support dot properties as sub tests. We don't do checking to make sure
+            // that the implied parent tests have been added. You must call them in
+            // order (either in the test, or make the parent test a dependency).
+            //
+            // Cap it to TWO to make the logic simple and because who needs that kind of subtesting
+            // hashtag famous last words
+            featureNameSplit = featureName.split('.');
+
+            if (featureNameSplit.length === 1) {
+              Modernizr[featureNameSplit[0]] = result;
+            } else {
+              // cast to a Boolean, if not one already
+              if (Modernizr[featureNameSplit[0]] && !(Modernizr[featureNameSplit[0]] instanceof Boolean)) {
+                Modernizr[featureNameSplit[0]] = new Boolean(Modernizr[featureNameSplit[0]]);
+              }
+
+              Modernizr[featureNameSplit[0]][featureNameSplit[1]] = result;
+            }
+
+            classes.push((result ? '' : 'no-') + featureNameSplit.join('-'));
+          }
+        }
+      }
+    }
+    ;
+
+    /**
+     * A convenience helper to check if the document we are running in is an SVG document
+     *
+     * @access private
+     * @returns {boolean}
+     */
+
+    var isSVG = docElement.nodeName.toLowerCase() === 'svg';
+    
+
+    /**
+     * setClasses takes an array of class names and adds them to the root element
+     *
+     * @access private
+     * @function setClasses
+     * @param {string[]} classes - Array of class names
+     */
+
+    // Pass in an and array of class names, e.g.:
+    //  ['no-webp', 'borderradius', ...]
+    function setClasses(classes) {
+      var className = docElement.className;
+      var classPrefix = Modernizr._config.classPrefix || '';
+
+      if (isSVG) {
+        className = className.baseVal;
+      }
+
+      // Change `no-js` to `js` (independently of the `enableClasses` option)
+      // Handle classPrefix on this too
+      if (Modernizr._config.enableJSClass) {
+        var reJS = new RegExp('(^|\\s)' + classPrefix + 'no-js(\\s|$)');
+        className = className.replace(reJS, '$1' + classPrefix + 'js$2');
+      }
+
+      if (Modernizr._config.enableClasses) {
+        // Add the new classes
+        className += ' ' + classPrefix + classes.join(' ' + classPrefix);
+        if (isSVG) {
+          docElement.className.baseVal = className;
+        } else {
+          docElement.className = className;
+        }
+      }
+
+    }
+
+    ;
+
+    /**
+     * hasOwnProp is a shim for hasOwnProperty that is needed for Safari 2.0 support
+     *
+     * @author kangax
+     * @access private
+     * @function hasOwnProp
+     * @param {object} object - The object to check for a property
+     * @param {string} property - The property to check for
+     * @returns {boolean}
+     */
+
+    // hasOwnProperty shim by kangax needed for Safari 2.0 support
+    var hasOwnProp;
+
+    (function() {
+      var _hasOwnProperty = ({}).hasOwnProperty;
+      /* istanbul ignore else */
+      /* we have no way of testing IE 5.5 or safari 2,
+      * so just assume the else gets hit */
+      if (!is(_hasOwnProperty, 'undefined') && !is(_hasOwnProperty.call, 'undefined')) {
+        hasOwnProp = function(object, property) {
+          return _hasOwnProperty.call(object, property);
+        };
+      }
+      else {
+        hasOwnProp = function(object, property) { /* yes, this can give false positives/negatives, but most of the time we don't care about those */
+          return ((property in object) && is(object.constructor.prototype[property], 'undefined'));
+        };
+      }
+    })();
+
+    
+
+
+    // _l tracks listeners for async tests, as well as tests that execute after the initial run
+    ModernizrProto._l = {};
+
+    /**
+     * Modernizr.on is a way to listen for the completion of async tests. Being
+     * asynchronous, they may not finish before your scripts run. As a result you
+     * will get a possibly false negative `undefined` value.
+     *
+     * @memberof Modernizr
+     * @name Modernizr.on
+     * @access public
+     * @function on
+     * @param {string} feature - String name of the feature detect
+     * @param {function} cb - Callback function returning a Boolean - true if feature is supported, false if not
+     * @example
+     *
+     * ```js
+     * Modernizr.on('flash', function( result ) {
+     *   if (result) {
+     *    // the browser has flash
+     *   } else {
+     *     // the browser does not have flash
+     *   }
+     * });
+     * ```
+     */
+
+    ModernizrProto.on = function(feature, cb) {
+      // Create the list of listeners if it doesn't exist
+      if (!this._l[feature]) {
+        this._l[feature] = [];
+      }
+
+      // Push this test on to the listener list
+      this._l[feature].push(cb);
+
+      // If it's already been resolved, trigger it on next tick
+      if (Modernizr.hasOwnProperty(feature)) {
+        // Next Tick
+        setTimeout(function() {
+          Modernizr._trigger(feature, Modernizr[feature]);
+        }, 0);
+      }
+    };
+
+    /**
+     * _trigger is the private function used to signal test completion and run any
+     * callbacks registered through [Modernizr.on](#modernizr-on)
+     *
+     * @memberof Modernizr
+     * @name Modernizr._trigger
+     * @access private
+     * @function _trigger
+     * @param {string} feature - string name of the feature detect
+     * @param {function|boolean} [res] - A feature detection function, or the boolean =
+     * result of a feature detection function
+     */
+
+    ModernizrProto._trigger = function(feature, res) {
+      if (!this._l[feature]) {
+        return;
+      }
+
+      var cbs = this._l[feature];
+
+      // Force async
+      setTimeout(function() {
+        var i, cb;
+        for (i = 0; i < cbs.length; i++) {
+          cb = cbs[i];
+          cb(res);
+        }
+      }, 0);
+
+      // Don't trigger these again
+      delete this._l[feature];
+    };
+
+    /**
+     * addTest allows you to define your own feature detects that are not currently
+     * included in Modernizr (under the covers it's the exact same code Modernizr
+     * uses for its own [feature detections](https://github.com/Modernizr/Modernizr/tree/master/feature-detects)). Just like the offical detects, the result
+     * will be added onto the Modernizr object, as well as an appropriate className set on
+     * the html element when configured to do so
+     *
+     * @memberof Modernizr
+     * @name Modernizr.addTest
+     * @optionName Modernizr.addTest()
+     * @optionProp addTest
+     * @access public
+     * @function addTest
+     * @param {string|object} feature - The string name of the feature detect, or an
+     * object of feature detect names and test
+     * @param {function|boolean} test - Function returning true if feature is supported,
+     * false if not. Otherwise a boolean representing the results of a feature detection
+     * @example
+     *
+     * The most common way of creating your own feature detects is by calling
+     * `Modernizr.addTest` with a string (preferably just lowercase, without any
+     * punctuation), and a function you want executed that will return a boolean result
+     *
+     * ```js
+     * Modernizr.addTest('itsTuesday', function() {
+     *  var d = new Date();
+     *  return d.getDay() === 2;
+     * });
+     * ```
+     *
+     * When the above is run, it will set Modernizr.itstuesday to `true` when it is tuesday,
+     * and to `false` every other day of the week. One thing to notice is that the names of
+     * feature detect functions are always lowercased when added to the Modernizr object. That
+     * means that `Modernizr.itsTuesday` will not exist, but `Modernizr.itstuesday` will.
+     *
+     *
+     *  Since we only look at the returned value from any feature detection function,
+     *  you do not need to actually use a function. For simple detections, just passing
+     *  in a statement that will return a boolean value works just fine.
+     *
+     * ```js
+     * Modernizr.addTest('hasJquery', 'jQuery' in window);
+     * ```
+     *
+     * Just like before, when the above runs `Modernizr.hasjquery` will be true if
+     * jQuery has been included on the page. Not using a function saves a small amount
+     * of overhead for the browser, as well as making your code much more readable.
+     *
+     * Finally, you also have the ability to pass in an object of feature names and
+     * their tests. This is handy if you want to add multiple detections in one go.
+     * The keys should always be a string, and the value can be either a boolean or
+     * function that returns a boolean.
+     *
+     * ```js
+     * var detects = {
+     *  'hasjquery': 'jQuery' in window,
+     *  'itstuesday': function() {
+     *    var d = new Date();
+     *    return d.getDay() === 2;
+     *  }
+     * }
+     *
+     * Modernizr.addTest(detects);
+     * ```
+     *
+     * There is really no difference between the first methods and this one, it is
+     * just a convenience to let you write more readable code.
+     */
+
+    function addTest(feature, test) {
+
+      if (typeof feature == 'object') {
+        for (var key in feature) {
+          if (hasOwnProp(feature, key)) {
+            addTest(key, feature[ key ]);
+          }
+        }
+      } else {
+
+        feature = feature.toLowerCase();
+        var featureNameSplit = feature.split('.');
+        var last = Modernizr[featureNameSplit[0]];
+
+        // Again, we don't check for parent test existence. Get that right, though.
+        if (featureNameSplit.length == 2) {
+          last = last[featureNameSplit[1]];
+        }
+
+        if (typeof last != 'undefined') {
+          // we're going to quit if you're trying to overwrite an existing test
+          // if we were to allow it, we'd do this:
+          //   var re = new RegExp("\\b(no-)?" + feature + "\\b");
+          //   docElement.className = docElement.className.replace( re, '' );
+          // but, no rly, stuff 'em.
+          return Modernizr;
+        }
+
+        test = typeof test == 'function' ? test() : test;
+
+        // Set the value (this is the magic, right here).
+        if (featureNameSplit.length == 1) {
+          Modernizr[featureNameSplit[0]] = test;
+        } else {
+          // cast to a Boolean, if not one already
+          if (Modernizr[featureNameSplit[0]] && !(Modernizr[featureNameSplit[0]] instanceof Boolean)) {
+            Modernizr[featureNameSplit[0]] = new Boolean(Modernizr[featureNameSplit[0]]);
+          }
+
+          Modernizr[featureNameSplit[0]][featureNameSplit[1]] = test;
+        }
+
+        // Set a single class (either `feature` or `no-feature`)
+        setClasses([(!!test && test != false ? '' : 'no-') + featureNameSplit.join('-')]);
+
+        // Trigger the event
+        Modernizr._trigger(feature, test);
+      }
+
+      return Modernizr; // allow chaining.
+    }
+
+    // After all the tests are run, add self to the Modernizr prototype
+    Modernizr._q.push(function() {
+      ModernizrProto.addTest = addTest;
+    });
+
+    
+
+  /*!
+  {
+    "name": "Webp",
+    "async": true,
+    "property": "webp",
+    "tags": ["image"],
+    "builderAliases": ["img_webp"],
+    "authors": ["Krister Kari", "@amandeep", "Rich Bradshaw", "Ryan Seddon", "Paul Irish"],
+    "notes": [{
+      "name": "Webp Info",
+      "href": "https://developers.google.com/speed/webp/"
+    }, {
+      "name": "Chormium blog - Chrome 32 Beta: Animated WebP images and faster Chrome for Android touch input",
+      "href": "https://blog.chromium.org/2013/11/chrome-32-beta-animated-webp-images-and.html"
+    }, {
+      "name": "Webp Lossless Spec",
+      "href": "https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification"
+    }, {
+      "name": "Article about WebP support on Android browsers",
+      "href": "http://www.wope-framework.com/en/2013/06/24/webp-support-on-android-browsers/"
+    }, {
+      "name": "Chormium WebP announcement",
+      "href": "https://blog.chromium.org/2011/11/lossless-and-transparency-encoding-in.html?m=1"
+    }]
+  }
+  !*/
+  /* DOC
+  Tests for lossy, non-alpha webp support.
+
+  Tests for all forms of webp support (lossless, lossy, alpha, and animated)..
+
+    Modernizr.webp              // Basic support (lossy)
+    Modernizr.webp.lossless     // Lossless
+    Modernizr.webp.alpha        // Alpha (both lossy and lossless)
+    Modernizr.webp.animation    // Animated WebP
+
+  */
+
+
+    Modernizr.addAsyncTest(function() {
+
+      var webpTests = [{
+        'uri': 'data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=',
+        'name': 'webp'
+      }, {
+        'uri': 'data:image/webp;base64,UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAwAAAABBxAR/Q9ERP8DAABWUDggGAAAADABAJ0BKgEAAQADADQlpAADcAD++/1QAA==',
+        'name': 'webp.alpha'
+      }, {
+        'uri': 'data:image/webp;base64,UklGRlIAAABXRUJQVlA4WAoAAAASAAAAAAAAAAAAQU5JTQYAAAD/////AABBTk1GJgAAAAAAAAAAAAAAAAAAAGQAAABWUDhMDQAAAC8AAAAQBxAREYiI/gcA',
+        'name': 'webp.animation'
+      }, {
+        'uri': 'data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA=',
+        'name': 'webp.lossless'
+      }];
+
+      var webp = webpTests.shift();
+      function test(name, uri, cb) {
+
+        var image = new Image();
+
+        function addResult(event) {
+          // if the event is from 'onload', check the see if the image's width is
+          // 1 pixel (which indiciates support). otherwise, it fails
+
+          var result = event && event.type === 'load' ? image.width == 1 : false;
+          var baseTest = name === 'webp';
+
+          // if it is the base test, and the result is false, just set a literal false
+          // rather than use the Boolean contrsuctor
+          addTest(name, (baseTest && result) ? new Boolean(result) : result);
+
+          if (cb) {
+            cb(event);
+          }
+        }
+
+        image.onerror = addResult;
+        image.onload = addResult;
+
+        image.src = uri;
+      }
+
+      // test for webp support in general
+      test(webp.name, webp.uri, function(e) {
+        // if the webp test loaded, test everything else.
+        if (e && e.type === 'load') {
+          for (var i = 0; i < webpTests.length; i++) {
+            test(webpTests[i].name, webpTests[i].uri);
+          }
+        }
+      });
+
+    });
+
+
+
+    // Run each test
+    testRunner();
+
+    // Remove the "no-js" class if it exists
+    setClasses(classes);
+
+    delete ModernizrProto.addTest;
+    delete ModernizrProto.addAsyncTest;
+
+    // Run the things that are supposed to run after the tests
+    for (var i = 0; i < Modernizr._q.length; i++) {
+      Modernizr._q[i]();
+    }
+
+    // Leak Modernizr namespace
+    window.Modernizr = Modernizr;
+
+
+  ;
+
+  })(window, document);
+});

+ 5 - 0
plugins/persistedstate.ts

@@ -0,0 +1,5 @@
+import { createNuxtPersistedState } from 'pinia-plugin-persistedstate/nuxt'
+
+export default defineNuxtPlugin(nuxtApp => {
+  nuxtApp.$pinia.use(createNuxtPersistedState(useCookie))
+})

+ 0 - 22
postcss.config.js

@@ -1,22 +0,0 @@
-module.exports = (ctx) => ({
-  map: false,//ctx.env === 'production' ? ctx.options.map : false,
-  parser: ctx.options.parser,
-  plugins: {
-    'postcss-import': { root: ctx.file.dirname },
-    'postcss-mixins': {},
-    // 'postcss-simple-vars': {},
-    'postcss-nested': {},
-    'tailwindcss': {},
-    'autoprefixer': {},
-    'postcss-preset-env': {},
-    cssnano: ctx.env !== 'production' ? false : {
-      preset: [
-        'default',
-        {
-          discardComments: { removeAll: true },
-          calc: false
-        }
-      ]
-    },
-  },
-})

+ 5 - 0
public/.htaccess

@@ -0,0 +1,5 @@
+RewriteEngine on
+
+RewriteRule "(^|/)\.(?!well-known\/)" 404 [L]
+RewriteRule "^.htaccess" 404 [L]
+RewriteRule "^.DS_Store" 404 [L]

+ 0 - 0
ads.txt → public/ads.txt


+ 0 - 0
files/FHCI/Marasoiu-supervision-1.pdf → public/files/FHCI/Marasoiu-supervision-1.pdf


+ 0 - 0
files/FHCI/Marasoiu-supervision-2.pdf → public/files/FHCI/Marasoiu-supervision-2.pdf


+ 0 - 0
files/FHCI/index.html → public/files/FHCI/index.html


+ 0 - 0
files/LC64_inkscape.pdf → public/files/LC64_inkscape.pdf


+ 0 - 0
files/LC67_inkscape.pdf → public/files/LC67_inkscape.pdf


+ 0 - 0
files/LC73_drupal.pdf → public/files/LC73_drupal.pdf


+ 0 - 0
files/LC74_drupal.pdf → public/files/LC74_drupal.pdf


+ 0 - 0
files/LC75_drupal3.pdf → public/files/LC75_drupal.pdf


+ 0 - 0
files/ProgC/supervision_1.html → public/files/ProgC/supervision_1.html


+ 1 - 1
files/ProgC/supervision_2.html → public/files/ProgC/supervision_2.html

@@ -48,7 +48,7 @@ code, pre {
     return 0;
 }
 </pre>
-	<p>Hint: heap allocation and deallocation should occur exactly once!</p>
+    <p>Hint: heap allocation and deallocation should occur exactly once!</p>
 
     <h2>6</h2>
     <p>Why should destructors in an abstract class almost always be declared <code>virtual</code>?</p>

+ 0 - 0
files/ProgC/supervision_3.html → public/files/ProgC/supervision_3.html


+ 0 - 0
files/ccers20-inequality-poster-combo.pdf → public/files/ccers20-inequality-poster-combo.pdf


+ 0 - 0
files/crusade_bsc_privacy_policy.html → public/files/crusade_bsc_privacy_policy.html


+ 0 - 0
files/csmc2017.pdf → public/files/csmc2017.pdf


+ 0 - 0
files/dmrn8-poster-combo.pdf → public/files/dmrn8-poster-combo.pdf


+ 0 - 0
files/franceschini-andrea-cv.pdf → public/files/franceschini-andrea-cv.pdf


+ 0 - 0
files/icmcsmc2014-poster.pdf → public/files/icmcsmc2014-poster.pdf


+ 0 - 0
files/icmcsmc2014.pdf → public/files/icmcsmc2014.pdf


+ 0 - 0
files/jmte-13-1.pdf → public/files/jmte-13-1.pdf


+ 0 - 0
files/las2019.pdf → public/files/las2019.pdf


+ 0 - 0
files/master_thesis.pdf → public/files/master_thesis.pdf


+ 0 - 0
files/met2016.pdf → public/files/met2016.pdf


+ 0 - 0
files/msca-2021-seal-of-excellence.pdf → public/files/msca-2021-seal-of-excellence.pdf


Some files were not shown because too many files changed in this diff