/* GENERATED by scripts/build-motion.mjs from motion/. Edit the sources, not this file. source:8c9c5a969366 */
/* core/tokens.css */
/* Shared values. Sections read these; they never redefine them. */
:root {
  --ease-out: cubic-bezier(.16, 1, .3, 1);
  --ease-ui: cubic-bezier(.4, 0, .2, 1);
  --m-z-progress: 6;
  --m-z-hud: 4;
}

/* Sweep position for heading wipes; registered so it can be transitioned */
@property --wipe {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 125%;
}

/* Distance sticky blocks keep from the top of a phone screen: below the header, or near the edge once the header has left */
@media (max-width: 760px) and (prefers-reduced-motion: no-preference) {
  html.motion { --m-top: 122px; }
  html.motion.header-away { --m-top: 18px; }
}

/* core/base.css */
/* Page-wide behaviour that belongs to no single section */
.scroll-progress { display: none; }

@media (prefers-reduced-motion: no-preference) {
  /* Sideways and tilted entrances must never widen the page */
  html.motion body:not(.dashboard-mode) main { overflow-x: clip; }

  /* Page scroll progress */
  html.motion body:not(.dashboard-mode) .scroll-progress {
    display: block;
    position: fixed;
    inset: 0 0 auto;
    height: 3px;
    z-index: var(--m-z-progress);
    background: var(--green);
    transform: scaleX(var(--m-progress, 0));
    transform-origin: right;
    pointer-events: none;
  }
  html.motion[dir="ltr"] body:not(.dashboard-mode) .scroll-progress { transform-origin: left; }

  /* Reveal on arrival: Motion.reveal() adds .rv below the fold and .in on arrival, then removes both */
  html.motion .rv {
    opacity: 0;
    transform: translateY(34px);
  }
  html.motion .rv.in {
    opacity: 1;
    transform: none;
    transition: opacity .8s var(--ease-out), transform .8s var(--ease-out);
    transition-delay: var(--m-delay, 0ms);
  }

  /* Touch feedback and dialogs */
  html.motion body:not(.dashboard-mode) :is(.primary, .plain, .text-link, .preferences button, .post button) {
    transition: transform .18s var(--ease-ui), background-color .2s, color .2s;
  }
  html.motion body:not(.dashboard-mode) :is(.primary, .plain, .text-link, .preferences button, .post button):active {
    transform: scale(.96);
  }
  html.motion body:not(.dashboard-mode) dialog[open] { animation: m-dialog-in .32s var(--ease-out); }
  html.motion body:not(.dashboard-mode) dialog[open]::backdrop { animation: m-fade-in .32s var(--ease-ui); }
  @keyframes m-dialog-in { from { opacity: 0; transform: translateY(18px); } }
  @keyframes m-fade-in { from { opacity: 0; } }
}

/* Phone header leaves on the way down and returns on the way up */
@media (max-width: 760px) and (prefers-reduced-motion: no-preference) {
  html.motion body:not(.dashboard-mode) header { transition: transform .3s var(--ease-ui); }
  html.motion.header-away body:not(.dashboard-mode) header:not(:focus-within) { transform: translateY(-100%); }
}

/* sections/hero.css */
/* Hero: load entrance (the v19 portrait curtain never showed on screen, so none is ported), scroll-out of heading and copy, portrait card that opens to full bleed on a phone and widens on wider screens.
   Values written by hero.js, each on the element that uses it (all optional, the defaults draw the resting state):
     --hero-s  0..1  scroll-out      on h1, .hero-heading, .intro, .actions
     --hero-p  0..1  portrait growth on figure.portrait
     --hero-is, --hero-py            on the portrait image (scale, parallax)
   Phone: hero.js adds .hero-pin to .hero; .hero then stacks as a column, the portrait is a sticky one-screen box, and .hero::after (a pseudo-element,
   no node) is the empty stretch it stays pinned over. A sticky box can only slide inside its container beyond its own margin box, hence the spacer.
   No node is created or moved: the figure stays a direct child of .hero, so the stat cms.js inserts before it is just another block above it.
   No external names. */

/* The owner's statistic counts up (hero.js adds hero-count to its number): equal-width digits keep the label from shifting. Not an animation, so it holds without motion too. */
.hero-stat strong.hero-count { font-variant-numeric: tabular-nums; }

/* Phone: the primary button takes the full row. Not a motion rule, so it also applies without motion. */
@media (max-width: 760px) {
  body:not(.dashboard-mode) .hero .actions .primary { flex: 1 1 100%; }
}

@media (prefers-reduced-motion: no-preference) {
  /* Heading, intro and buttons leave at three rates while the first screen scrolls away. The buttons are never faded. */
  html.motion body:not(.dashboard-mode) .hero h1 {
    transform-origin: 100% 0;
    transform: translateY(calc(var(--hero-s, 0) * 96px)) scale(calc(1 - var(--hero-s, 0) * .1));
    opacity: calc(1.15 - var(--hero-s, 0) * 1.2);
  }
  html.motion[dir="ltr"] body:not(.dashboard-mode) .hero h1 {
    transform-origin: 0 0;
  }
  html.motion body:not(.dashboard-mode) .hero-copy .intro {
    transform: translateY(calc(var(--hero-s, 0) * 48px));
    opacity: calc(1.2 - var(--hero-s, 0) * 1.5);
  }
  html.motion body:not(.dashboard-mode) .hero-copy .actions {
    transform: translateY(calc(var(--hero-s, 0) * 18px));
  }

  /* A green rule above the heading: draws in on load, then stretches and fades with the scroll */
  html.motion body:not(.dashboard-mode) .hero-heading { position: relative; }
  html.motion body:not(.dashboard-mode) .hero-heading::before {
    content: "";
    position: absolute;
    inset-inline-start: 0;
    top: -20px;
    width: 56px;
    height: 4px;
    border-radius: 4px;
    background: var(--green);
    pointer-events: none;
    transform-origin: 100% 0;
    transform: scaleX(calc(1 + var(--hero-s, 0) * 4));
    opacity: calc(1 - var(--hero-s, 0));
  }
  html.motion[dir="ltr"] body:not(.dashboard-mode) .hero-heading::before { transform-origin: 0 0; }

  /* Load entrance. html.pre holds the start state; removing it starts the animations. Backwards fill only, so the scroll values take over afterwards. */
  html.motion.pre body:not(.dashboard-mode) :is(.hero-copy .intro, .hero-copy .actions) { opacity: 0; }
  html.motion:not(.pre) body:not(.dashboard-mode) .hero-copy .intro { animation: hero-rise 1s var(--ease-out) .4s backwards; }
  html.motion:not(.pre) body:not(.dashboard-mode) .hero-copy .actions > * { animation: hero-pop .9s var(--ease-out) backwards; }
  html.motion:not(.pre) body:not(.dashboard-mode) .hero-copy .actions > :nth-child(1) { animation-delay: .6s; }
  html.motion:not(.pre) body:not(.dashboard-mode) .hero-copy .actions > :nth-child(2) { animation-delay: .72s; }
  html.motion:not(.pre) body:not(.dashboard-mode) .hero-copy .actions > :nth-child(3) { animation-delay: .84s; }
  html.motion:not(.pre) body:not(.dashboard-mode) .hero-heading::before { animation: hero-rule 1s var(--ease-out) .15s backwards; }
  @keyframes hero-rise { from { opacity: 0; transform: translateY(46px); filter: blur(6px); } }
  @keyframes hero-pop { from { opacity: 0; transform: translateY(38px) scale(.88); } }
  @keyframes hero-rule { from { transform: scaleX(0); } }
}

/* Tablet and desktop: the two column hero stays. The portrait widens and rounds off as it arrives, the image settles from a push-in. */
@media (min-width: 761px) and (prefers-reduced-motion: no-preference) {
  html.motion body:not(.dashboard-mode) .hero .portrait {
    clip-path: inset(0 calc((1 - var(--hero-p, 1)) * 9%) round calc(12px + (1 - var(--hero-p, 1)) * 22px));
  }
  html.motion body:not(.dashboard-mode) .hero .portrait img {
    transform: translateY(var(--hero-py, 0px)) scale(var(--hero-is, 1));
  }
}

/* Phone: pinned stage. The box is one screen tall and clipped to a card; while the page scrolls the clip opens to the full screen,
   the corner radius goes to zero and the image behind it pushes in. */
@media (max-width: 760px) and (prefers-reduced-motion: no-preference) {
  html.motion body:not(.dashboard-mode) .hero.hero-pin .portrait {
    --hero-card: 300px;
    --hero-m: calc((100vh - var(--hero-card)) / 2);
    position: sticky;
    top: 0;
    z-index: 1;
    height: 100vh;
    margin-inline: -24px;
    margin-top: calc(-1 * var(--hero-m));
    margin-bottom: 0;
    border-radius: 0;
    clip-path: inset(calc(var(--hero-m) * (1 - var(--hero-p, 0))) calc(24px * (1 - var(--hero-p, 0))) calc(var(--hero-m) * (1 - var(--hero-p, 0))) calc(24px * (1 - var(--hero-p, 0))) round calc(16px * (1 - var(--hero-p, 0))));
  }
  @supports (height: 100svh) {
    html.motion body:not(.dashboard-mode) .hero.hero-pin .portrait {
      --hero-m: calc((100svh - var(--hero-card)) / 2);
      height: 100svh;
    }
  }
  html.motion body:not(.dashboard-mode) .hero.hero-pin { display: flex; flex-direction: column; align-items: stretch; }
  html.motion body:not(.dashboard-mode) .hero.hero-pin::after { content: ""; display: block; flex: none; height: 58vh; margin-top: -28px; pointer-events: none; }
  @supports (height: 100svh) {
    html.motion body:not(.dashboard-mode) .hero.hero-pin::after { height: 58svh; }
  }
  html.motion body:not(.dashboard-mode) .hero.hero-pin .portrait img {
    position: absolute;
    left: 50%;
    top: 50%;
    width: auto;
    max-width: none;
    height: var(--hero-card);
    aspect-ratio: 2048/1143;
    object-fit: cover;
    transform: translate(-50%, -50%) translateY(var(--hero-py, 0px)) scale(var(--hero-is, 1));
  }
  /* Dark veil that grows with the portrait. Built from the feature block colour, which stays dark olive in both themes, darkened with black to match v19's #0b1a0b. */
  html.motion body:not(.dashboard-mode) .hero.hero-pin .portrait::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: linear-gradient(to top, color-mix(in srgb, color-mix(in srgb, var(--feature-bg) 45%, black) 80%, transparent), transparent 55%);
    opacity: calc(var(--hero-p, 0) * .65);
  }
}

/* sections/topics.css */
/* topics: dark ticker band with two rows (the script moves the rows; this file only dresses them).
   Nodes created by the script: div.topics-track.topics-main and div.topics-track.topics-sub inside .topics.
   Touches no names from the site or the core other than .topics and the site colour variables.
*/
@media (prefers-reduced-motion: no-preference) {
  html.motion body:not(.dashboard-mode) .topics.topics-on {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
    overflow: hidden;
    background: var(--feature-bg);
    border: 0;
    border-radius: 14px;
    padding-block: 20px 16px;
    margin-block: 10px;
  }
  /* Fades at both ends */
  html.motion body:not(.dashboard-mode) .topics.topics-on::before,
  html.motion body:not(.dashboard-mode) .topics.topics-on::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    width: 16%;
    z-index: 2;
    pointer-events: none;
  }
  html.motion body:not(.dashboard-mode) .topics.topics-on::before { left: 0; background: linear-gradient(to right, var(--feature-bg), transparent); }
  html.motion body:not(.dashboard-mode) .topics.topics-on::after { right: 0; background: linear-gradient(to left, var(--feature-bg), transparent); }

  html.motion body:not(.dashboard-mode) .topics.topics-on .topics-track { display: flex; width: max-content; }
  /* A promoted layer only while the band is near the screen */
  html.motion body:not(.dashboard-mode) .topics.topics-live .topics-track { will-change: transform; }

  html.motion body:not(.dashboard-mode) .topics-track span {
    display: inline-flex;
    align-items: center;
    white-space: nowrap;
  }
  html.motion body:not(.dashboard-mode) .topics-track span::after {
    content: "\2726";
    display: inline-block;
    font-size: .5em;
    color: var(--feature-label);
    transform: rotate(calc(var(--topics-spin, 0) * 1deg));
  }

  html.motion body:not(.dashboard-mode) .topics-main span {
    gap: 22px;
    padding-inline-start: 22px;
    font-size: clamp(2.1rem, 4.6vw, 3.6rem);
    font-weight: 700;
    line-height: 1.55;
    color: var(--feature-text);
  }
  html.motion body:not(.dashboard-mode) .topics-main span:nth-child(even) { color: var(--feature-label); }

  html.motion body:not(.dashboard-mode) .topics-sub span {
    gap: 18px;
    padding-inline-start: 18px;
    font-size: 1.05rem;
    font-weight: 500;
    line-height: 1.7;
    color: var(--feature-label);
    opacity: .85;
  }
  html.motion body:not(.dashboard-mode) .topics-sub span:nth-child(even) { color: var(--feature-text); }
}

/* Phones: the band runs edge to edge */
@media (max-width: 760px) and (prefers-reduced-motion: no-preference) {
  html.motion body:not(.dashboard-mode) .topics.topics-on {
    margin-inline: -24px;
    border-radius: 0;
  }
}

/* sections/about.css */
/* about: section#about. Kicker line, line-by-line paragraph, skill chips.
   Nodes created: none. Hidden starting states are added by the script (about-armed), never here.
   @external none
*/
@media (prefers-reduced-motion: no-preference) {
  /* Kicker line draws in */
  html.motion body:not(.dashboard-mode) .about-kick::before {
    content: "";
    display: inline-block;
    width: 34px;
    height: 2px;
    margin-inline-end: 10px;
    vertical-align: middle;
    background: var(--green);
    transform: scaleX(var(--about-k, 1));
    transform-origin: right;
  }
  html.motion[dir="ltr"] body:not(.dashboard-mode) .about-kick::before { transform-origin: left; }

  /* Paragraph lights up line by line: --about-f is the front position, --about-r the progress of the side bar */
  html.motion body:not(.dashboard-mode) .about-sweep {
    --about-dim: color-mix(in srgb, var(--ink) 24%, transparent);
    position: relative;
    color: transparent;
    -webkit-text-fill-color: transparent;
    background-image: linear-gradient(to bottom, var(--ink) 0, var(--ink) calc(var(--about-f, 125%) - 16%), var(--green) var(--about-f, 125%), var(--about-dim) calc(var(--about-f, 125%) + .5%), var(--about-dim) 100%);
    -webkit-background-clip: text;
    background-clip: text;
  }
  html.motion body:not(.dashboard-mode) .about p.about-sweep::before {
    content: "";
    position: absolute;
    inset-block: .45em;
    inset-inline-start: -14px;
    width: 3px;
    border-radius: 3px;
    background: linear-gradient(to bottom, var(--green) var(--about-r, 100%), var(--line) var(--about-r, 100%));
  }

  /* Skill chips pop in one after another */
  html.motion body:not(.dashboard-mode) .skill-list.about-armed span {
    font-weight: 600;
    border-color: var(--green);
    opacity: 0;
    transform: translateY(28px) scale(.35) rotate(-10deg);
    transition: transform .3s var(--ease-ui), opacity .2s;
  }
  html.motion body:not(.dashboard-mode) .skill-list.about-armed span.about-in {
    opacity: 1;
    transform: none;
    transition: transform .85s cubic-bezier(.34, 1.8, .5, 1) calc(var(--about-ci, 0) * 120ms), opacity .3s ease calc(var(--about-ci, 0) * 120ms);
    animation: about-flash 1.2s ease calc(var(--about-ci, 0) * 120ms + 200ms) backwards;
  }
  @keyframes about-flash {
    from { background: var(--button-bg); color: var(--button-text); border-color: var(--button-bg); }
  }
}

/* sections/services.css */
/* services: #services .section-head and #services .cards .service. Kicker line, paragraph sweep; on phones a deck of sticky cards plus a fixed counter.
   Nodes created by the script: div.icon.services-ico in cards that have no icon, div.services-hud on body (fixed counter outside the section).
   Hidden starting states are added by the script, never here.
   @external icon, --m-top
*/
.services-hud { display: none; }

@media (prefers-reduced-motion: no-preference) {
  /* The distance between stacked cards: the one place this number lives. The script reads it from here. */
  html.motion body:not(.dashboard-mode) #services .cards { --services-step: 14px; }

  /* Kicker line draws in */
  html.motion body:not(.dashboard-mode) .services-kick::before {
    content: "";
    display: inline-block;
    width: 34px;
    height: 2px;
    margin-inline-end: 10px;
    vertical-align: middle;
    background: var(--green);
    transform: scaleX(var(--services-k, 1));
    transform-origin: right;
  }
  html.motion[dir="ltr"] body:not(.dashboard-mode) .services-kick::before { transform-origin: left; }

  /* Intro paragraph lights up line by line with scroll */
  html.motion body:not(.dashboard-mode) .services-sweep {
    --services-dim: color-mix(in srgb, var(--ink) 24%, transparent);
    position: relative;
    color: transparent;
    -webkit-text-fill-color: transparent;
    background-image: linear-gradient(to bottom, var(--ink) 0, var(--ink) calc(var(--services-f, 125%) - 16%), var(--green) var(--services-f, 125%), var(--services-dim) calc(var(--services-f, 125%) + .5%), var(--services-dim) 100%);
    -webkit-background-clip: text;
    background-clip: text;
  }

  /* Line icons: the strokes are hidden by the script's own markup and draw once when the icon gets services-drawn */
  html.motion body:not(.dashboard-mode) #services .services-svg {
    width: 62%;
    height: 62%;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
    overflow: visible;
  }
  html.motion body:not(.dashboard-mode) #services .services-line {
    stroke-dasharray: var(--services-len);
    stroke-dashoffset: var(--services-len);
  }
  html.motion body:not(.dashboard-mode) #services .services-drawn .services-line {
    stroke-dashoffset: 0;
    transition: stroke-dashoffset .8s var(--ease-out) calc(var(--services-ix, 0) * .22s + .15s);
  }
  html.motion body:not(.dashboard-mode) #services .services-drawn .services-svg { animation: services-settle .7s var(--ease-out) .1s backwards; }
  @keyframes services-settle {
    from { transform: scale(.82) rotate(-8deg); }
  }

  @keyframes services-ring {
    0% { opacity: .85; transform: scale(1); }
    100% { opacity: 0; transform: scale(1.9); }
  }
}

/* Phones: the deck. --services-c is how far the next card covers this one, --services-e how far this card is from its stuck position. */
@media (max-width: 760px) and (prefers-reduced-motion: no-preference) {
  html.motion body:not(.dashboard-mode) #services .cards { counter-reset: services-n; }
  html.motion body:not(.dashboard-mode) #services .service {
    --services-s: -1;
    counter-increment: services-n;
    position: sticky;
    top: calc(var(--m-top) + var(--services-i, 0) * var(--services-step));
    min-height: min(60svh, 460px);
    transform-origin: 50% 0;
    transform: perspective(1100px)
      translate3d(0, calc(var(--services-e, 0) * 46px), 0)
      rotate(calc(var(--services-s) * (var(--services-e, 0) * 6deg + var(--services-c, 0) * 1.6deg)))
      rotateX(calc(var(--services-e, 0) * -16deg + var(--services-c, 0) * 9deg))
      scale(calc(1 - var(--services-c, 0) * .085 - var(--services-e, 0) * .07));
    box-shadow: 0 -16px 36px rgb(0 0 0 / .12), 0 26px 44px -26px rgb(0 0 0 / .4);
    transition: top .3s var(--ease-ui), border-color .2s;
  }
  html.motion body:not(.dashboard-mode) #services .service:nth-child(2) { --services-i: 1; }
  html.motion body:not(.dashboard-mode) #services .service:nth-child(3) { --services-i: 2; }
  html.motion body:not(.dashboard-mode) #services .service:nth-child(4) { --services-i: 3; }
  html.motion body:not(.dashboard-mode) #services .service:nth-child(n+5) { --services-i: 4; }
  html.motion body:not(.dashboard-mode) #services .service:nth-child(even) { --services-s: 1; }
  /* A promoted layer only while the deck is near the screen */
  html.motion body:not(.dashboard-mode) #services .cards.services-live .service { will-change: transform; }
  html.motion body:not(.dashboard-mode) #services .service.services-on { border-color: var(--green); }

  html.motion body:not(.dashboard-mode) #services .service::before {
    content: counter(services-n, decimal-leading-zero);
    position: absolute;
    inset-block-start: 12px;
    inset-inline-end: 22px;
    font: 700 3.8rem/1 Georgia, serif;
    color: var(--green);
    pointer-events: none;
    opacity: calc(.18 * (1 - var(--services-e, 0)));
    transform: translate3d(0, calc(var(--services-e, 0) * 36px), 0);
  }
  html.motion body:not(.dashboard-mode) #services .service::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: #000;
    opacity: calc(var(--services-c, 0) * .3);
    pointer-events: none;
  }
  html.motion body:not(.dashboard-mode) #services .service .icon {
    position: relative;
    width: 60px;
    height: 60px;
    font-size: 1.7rem;
    transform: rotate(calc(var(--services-e, 0) * -220deg + var(--services-c, 0) * 40deg)) scale(calc(1 - var(--services-e, 0) * .55));
  }
  html.motion body:not(.dashboard-mode) #services .service .icon::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 50%;
    border: 2px solid var(--green);
    opacity: 0;
    pointer-events: none;
  }
  html.motion body:not(.dashboard-mode) #services .service.services-on .icon::after { animation: services-ring 1.8s var(--ease-out) infinite; }
  html.motion body:not(.dashboard-mode) #services .service h3 {
    opacity: calc(1 - var(--services-e, 0) * 1.1);
    transform: translate3d(0, calc(var(--services-e, 0) * 22px), 0);
  }
  html.motion body:not(.dashboard-mode) #services .service p {
    opacity: calc(1 - var(--services-e, 0) * 1.4);
    transform: translate3d(0, calc(var(--services-e, 0) * 38px), 0);
  }
  html.motion body:not(.dashboard-mode) #services .service > .plain {
    opacity: calc(1 - var(--services-e, 0) * 1.7);
    transform: translate3d(0, calc(var(--services-e, 0) * 54px), 0);
  }

  /* Fixed counter */
  .services-hud {
    display: flex;
    position: fixed;
    inset-inline: 0;
    bottom: calc(14px + env(safe-area-inset-bottom));
    margin-inline: auto;
    width: max-content;
    z-index: var(--m-z-hud);
    align-items: center;
    gap: 10px;
    padding: 10px 18px;
    border-radius: 99px;
    background: var(--feature-bg);
    color: var(--feature-text);
    box-shadow: 0 10px 30px rgb(0 0 0 / .3);
    font: 600 1rem/1 Georgia, serif;
    direction: ltr;
    transform: translateY(170%);
    opacity: 0;
    transition: transform .5s var(--ease-out), opacity .3s;
    pointer-events: none;
  }
  .services-hud.services-show { transform: none; opacity: 1; }
  .services-hud b { font-size: 1.25rem; display: inline-block; }
  .services-hud span { opacity: .65; }
  .services-hud i { display: flex; gap: 5px; margin-inline-start: 4px; }
  .services-hud u {
    display: block;
    width: 14px;
    height: 3px;
    border-radius: 3px;
    background: var(--feature-text);
    opacity: .3;
    transition: transform .4s var(--ease-out), opacity .3s, background-color .3s;
  }
  .services-hud u.services-lit { opacity: 1; transform: scaleX(1.9); background: var(--feature-label); }
}

/* sections/works.css */
/* Portfolio: .work-heading + .work-grid + .work-card.
   @external work-heading, work-grid, work-card, rv, in
   Nodes works.js creates (siblings after .work-grid): .works-meta (.works-count b/span, .works-hint svg/em), .works-progress (i).
   Grid classes: works-on (row is a scrollable reel), works-live (near the screen), works-seen, works-touched.
   Per-card custom properties (written on the card): --works-d --works-f (reel position), --works-bd --works-bf (card tilt), --works-in (entrance), --works-z.
   Heading: --works-h. Nudge: a compositor animation of the individual `translate` property, no custom property. */
/* Every per-frame property is registered as non-inherited: a changing value stays on the element that uses it and never reaches the
   shadow tree inside each card's video controls. Children that need a parent's value say so with "inherit". */
@property --works-d { syntax: '<number>'; inherits: false; initial-value: 0; }
@property --works-f { syntax: '<number>'; inherits: false; initial-value: 1; }
@property --works-in { syntax: '<number>'; inherits: false; initial-value: 1; }
@property --works-bd { syntax: '<number>'; inherits: false; initial-value: 0; }
@property --works-bf { syntax: '<number>'; inherits: false; initial-value: 1; }
@property --works-z { syntax: '<number>'; inherits: false; initial-value: 30; }
@property --works-xf { syntax: '<transform-list>'; inherits: false; initial-value: none; }

.works-meta { display: none; }
.works-progress { display: none; }

/* Phone: one swipeable row instead of a 14 screen column. Runs for every visitor, motion or not. */
@media (max-width: 760px) {
  body:not(.dashboard-mode) .work-heading { margin-bottom: 6px; }
  body:not(.dashboard-mode) .work-grid {
    position: relative;
    display: flex;
    gap: 14px;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    margin-inline: -24px;
    padding: 30px 18vw 62px;
    scrollbar-width: none;
  }
  body:not(.dashboard-mode) .work-grid::-webkit-scrollbar { display: none; }
  body:not(.dashboard-mode) .work-card { position: relative; flex: 0 0 64vw; max-width: none; margin: 0; scroll-snap-align: center; }
  body:not(.dashboard-mode) .work-card h4 { text-align: center; margin-top: 16px; }

  /* Counter and swipe hint */
  body:not(.dashboard-mode) .works-meta { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; margin-top: -30px; min-height: 56px; }
  body:not(.dashboard-mode) .works-meta[hidden] { display: none; }
  .works-count { display: flex; align-items: baseline; gap: 8px; direction: ltr; font-variant-numeric: tabular-nums; color: var(--ink); }
  .works-count b { display: block; font-size: 2.7rem; line-height: 1; font-weight: 600; letter-spacing: -.02em; }
  .works-count span { font-size: 1rem; color: var(--muted); font-weight: 500; }
  .works-hint { display: flex; align-items: center; gap: 8px; font-size: .875rem; color: var(--muted); padding: 8px 14px; border: 1px solid var(--line); border-radius: 999px; background: var(--surface); pointer-events: none; transition: opacity .45s var(--ease-ui), transform .45s var(--ease-ui); }
  .works-hint em { font-style: normal; }
  .works-hint svg { width: 22px; height: 14px; flex: none; color: var(--green); }
  html[dir="ltr"] .works-hint svg { transform: scaleX(-1); }
  .works-hint.works-gone { opacity: 0; transform: translateY(8px); }

  /* Progress bar under the row */
  body:not(.dashboard-mode) .works-progress { display: block; height: 4px; margin-top: 12px; border-radius: 3px; background: var(--line); overflow: hidden; }
  body:not(.dashboard-mode) .works-progress[hidden] { display: none; }
  body:not(.dashboard-mode) .works-progress i { display: block; height: 100%; border-radius: 3px; background: var(--green); transform: scaleX(var(--works-p, .2)); transform-origin: right; transition: transform .2s var(--ease-ui); }
  html[dir="ltr"] body:not(.dashboard-mode) .works-progress i { transform-origin: left; }
}

@media (max-width: 760px) and (prefers-reduced-motion: no-preference) {
  html.motion .works-count b { animation: works-roll .42s var(--ease-out); }
  @keyframes works-roll { from { opacity: 0; transform: translateY(55%) scale(.9); } }
  html.motion .work-grid.works-live + .works-meta .works-hint svg { animation: works-chev 1.3s ease-in-out infinite; }
  @keyframes works-chev { 0%, 100% { transform: translateX(0); opacity: .5; } 50% { transform: translateX(-7px); opacity: 1; } }
  html.motion[dir="ltr"] .work-grid.works-live + .works-meta .works-hint svg { animation-name: works-chev-l; }
  @keyframes works-chev-l { 0%, 100% { transform: scaleX(-1) translateX(0); opacity: .5; } 50% { transform: scaleX(-1) translateX(-7px); opacity: 1; } }

  /* The reel: shared perspective, cards fan out from one pile as the page scrolls in (--works-in 0..1) */
  /* Nudge before the first touch: the card's layers slide together. Runs only while the row is near the screen. */
  html.motion body:not(.dashboard-mode) .work-grid.works-on.works-live.works-seen:not(.works-touched) > .work-card :is(video, h4),
  html.motion body:not(.dashboard-mode) .work-grid.works-on.works-live.works-seen:not(.works-touched) > .work-card::before,
  html.motion body:not(.dashboard-mode) .work-grid.works-on.works-live.works-seen:not(.works-touched) > .work-card::after { animation: works-nudge 3.4s ease-in-out .8s infinite; }
  html.motion[dir="ltr"] body:not(.dashboard-mode) .work-grid.works-on.works-live.works-seen:not(.works-touched) > .work-card :is(video, h4),
  html.motion[dir="ltr"] body:not(.dashboard-mode) .work-grid.works-on.works-live.works-seen:not(.works-touched) > .work-card::before,
  html.motion[dir="ltr"] body:not(.dashboard-mode) .work-grid.works-on.works-live.works-seen:not(.works-touched) > .work-card::after { animation-name: works-nudge-l; }
  @keyframes works-nudge { 0%, 55%, 100% { translate: 0 0; } 22% { translate: 30px 0; } 34% { translate: 22px 0; } }
  @keyframes works-nudge-l { 0%, 55%, 100% { translate: 0 0; } 22% { translate: -30px 0; } 34% { translate: -22px 0; } }

  /* The card tilts and dims with its distance from the centre. The glow, floor reflection and title ride the same transform (--works-xf);
     the video carries it, the card itself only tilts, because a heavily transformed card moves its scroll-snap points while it is swiped */
  html.motion body:not(.dashboard-mode) .work-grid.works-on > .work-card {
    --works-w: 64vw;
    --works-hh: calc(var(--works-w) * 1.7778);
    --works-xf: perspective(1000px) translate3d(calc(var(--works-d) * (-.40 * var(--works-w) - (1 - var(--works-in)) * .85 * (var(--works-w) + 14px))), calc((1 - var(--works-in)) * 170px - var(--works-f) * 12px), calc(-1 * (1 - var(--works-f)) * 130px - (1 - var(--works-in)) * 420px)) rotateX(calc((1 - var(--works-in)) * 32deg)) rotateY(calc(var(--works-d) * -42deg)) rotateZ(calc((1 - var(--works-in)) * var(--works-d) * -26deg)) scale(calc(.8 + .28 * var(--works-f)));
    z-index: var(--works-z);
    transform: perspective(900px) rotateY(calc(var(--works-bd) * -34deg)) scale(calc(.84 + .16 * var(--works-bf)));
    opacity: calc((.6 + .4 * var(--works-f)) * min(1, var(--works-in) * 2.5));
    filter: brightness(calc(.66 + .34 * var(--works-f)));
  }
  html.motion body:not(.dashboard-mode) .work-grid.works-on > .work-card > video { --works-xf: inherit; --works-f: inherit; transform: var(--works-xf); transform-origin: 50% 50%; border-color: color-mix(in srgb, var(--green) calc(var(--works-f) * 100%), var(--line)); box-shadow: 0 22px 44px -14px #0000005c; }
  /* Glow behind the hero card and a floor reflection under it */
  html.motion body:not(.dashboard-mode) .work-grid.works-on > .work-card::before { --works-xf: inherit; --works-f: inherit; content: ""; position: absolute; z-index: -1; inset: -30px -22px 10px; border-radius: 40px; pointer-events: none; background: radial-gradient(65% 55% at 50% 48%, color-mix(in srgb, var(--green) 78%, transparent), transparent 74%); opacity: calc(var(--works-f) * var(--works-f) * .9); transform: var(--works-xf); transform-origin: 50% calc(var(--works-hh) / 2 + 30px); }
  html.motion body:not(.dashboard-mode) .work-grid.works-on > .work-card::after { --works-xf: inherit; --works-f: inherit; content: ""; position: absolute; z-index: -1; left: 6%; right: 6%; top: calc(var(--works-hh) - 10px); height: 34px; pointer-events: none; background: radial-gradient(50% 50% at 50% 50%, color-mix(in srgb, var(--green) 70%, transparent), transparent 75%); opacity: calc(var(--works-f) * .8); transform: var(--works-xf) scaleY(.6); transform-origin: 50% calc(10px - var(--works-hh) / 2); }
  /* Title follows focus: wipe in reading direction, rise, and a rule that grows */
  html.motion body:not(.dashboard-mode) .work-grid.works-on > .work-card h4 { --works-xf: inherit; --works-f: inherit; color: var(--ink); font-size: 1.0625rem; font-weight: 600; opacity: var(--works-f); transform: var(--works-xf) translateY(calc((1 - var(--works-f)) * 16px)); transform-origin: 50% calc(-1 * (var(--works-hh) / 2 + 16px)); clip-path: inset(-.4em 0 -.4em calc((1 - min(1, var(--works-f) * 1.3)) * 100%)); }
  html.motion[dir="ltr"] body:not(.dashboard-mode) .work-grid.works-on > .work-card h4 { clip-path: inset(-.4em calc((1 - min(1, var(--works-f) * 1.3)) * 100%) -.4em 0); }
  html.motion body:not(.dashboard-mode) .work-grid.works-on > .work-card h4::after { --works-f: inherit; content: ""; display: block; width: 34px; height: 3px; border-radius: 3px; margin: 8px auto 0; background: var(--green); transform: scaleX(var(--works-f)); transform-origin: 50% 50%; }

  /* Heading scrubs with the page scroll (--works-h 0..1, written on .work-heading) */
  html.motion body:not(.dashboard-mode) .work-heading h3 { transform: translateY(calc((1 - var(--works-h, 1)) * 38px)); clip-path: inset(-.3em 0 -.3em calc((1 - var(--works-h, 1)) * 100%)); opacity: calc(.2 + .8 * var(--works-h, 1)); }
  html.motion[dir="ltr"] body:not(.dashboard-mode) .work-heading h3 { clip-path: inset(-.3em calc((1 - var(--works-h, 1)) * 100%) -.3em 0); }
  html.motion body:not(.dashboard-mode) .work-heading p { transform: translateY(calc((1 - var(--works-h, 1)) * 26px)); opacity: var(--works-h, 1); }
  html.motion body:not(.dashboard-mode) .work-heading::after { content: ""; display: block; width: 72px; height: 3px; border-radius: 3px; margin-top: 16px; background: var(--green); transform: scaleX(var(--works-h, 1)); transform-origin: right; }
  html.motion[dir="ltr"] body:not(.dashboard-mode) .work-heading::after { transform-origin: left; }
}

/* Grid (768 and up): cards below the fold rise in (Motion.reveal adds .rv, then .in on arrival) */
@media (min-width: 761px) and (prefers-reduced-motion: no-preference) {
  html.motion body:not(.dashboard-mode) .work-grid > .work-card.rv:not(.in) { transform: translateY(46px) scale(.96); }
}

/* sections/workshop.css */
/* workshop: the pinned card that opens as it arrives, in step with the scroll.
   Owns: .workshop and everything inside it, the spacer under the card.
   Nodes created by workshop.js: div.workshop-pin (spacer after the card; progress is measured from it so the card's own transforms never feed back).
   State class: workshop-live on #workshops, added by script. Without it nothing here applies and the card is simply there.
   Properties, each written on the element that reads it: --workshop-a card opens, --workshop-p raw 0..1, --workshop-b body arrives, --workshop-d text arrives, --workshop-c tag and footer land.
   --workshop-s is +1 in RTL and -1 in LTR (the inline-start side as an X sign). Every variable falls back to its final state.
   @external --m-top, --ease-out, --ease-ui */
.workshop-pin { height: 0; }

#workshops.workshop-live { --workshop-s: 1; }
html[dir="ltr"] #workshops.workshop-live { --workshop-s: -1; }

#workshops.workshop-live .workshop {
  transform: perspective(1200px) rotateX(calc((1 - var(--workshop-a, 1)) * 14deg)) scale(calc(.9 + .1 * var(--workshop-a, 1)));
  transform-origin: 50% 100%;
  clip-path: inset(calc((1 - var(--workshop-a, 1)) * 36%) calc((1 - var(--workshop-a, 1)) * 7%) round calc(12px + (1 - var(--workshop-a, 1)) * 30px));
}
#workshops.workshop-live .workshop-label {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  transform: translate3d(calc(var(--workshop-s) * (1 - var(--workshop-a, 1)) * 80px), 0, 0);
  opacity: calc(.2 + var(--workshop-a, 1));
}
#workshops.workshop-live .workshop-label::after {
  content: "";
  position: absolute;
  z-index: -1;
  width: 170px;
  height: 170px;
  border-radius: 50%;
  border: 1.5px dashed currentColor;
  outline: 1px solid currentColor;
  outline-offset: 20px;
  inset-inline-end: -46px;
  top: -58px;
  opacity: .26;
  transform: rotate(calc(var(--workshop-p, 1) * 220deg)) scale(calc(.55 + .45 * var(--workshop-a, 1)));
}
#workshops.workshop-live .workshop-label .large {
  display: block;
  transform-origin: 0 50%;
  transform: translate3d(calc(var(--workshop-s) * ((1 - var(--workshop-a, 1)) * -120px + (var(--workshop-p, 1) - 1) * 18px)), 0, 0) scale(calc(1 + (1 - var(--workshop-a, 1)) * .35));
  opacity: calc(.1 + var(--workshop-a, 1));
}
#workshops.workshop-live .workshop-label span { opacity: var(--workshop-a, 1); }
#workshops.workshop-live .workshop-body {
  transform: translate3d(calc(var(--workshop-s) * (1 - var(--workshop-b, 1)) * -80px), 0, 0);
  opacity: calc(.1 + var(--workshop-b, 1));
}
#workshops.workshop-live .workshop-body :is(h3, p) {
  opacity: var(--workshop-d, 1);
  transform: translateY(calc((1 - var(--workshop-d, 1)) * 34px));
}
#workshops.workshop-live .workshop-body p { transform: translateY(calc((1 - var(--workshop-d, 1)) * 52px)); }
#workshops.workshop-live .workshop-body .tag {
  transform-origin: 50% 100%;
  opacity: var(--workshop-c, 1);
  transform: translateY(calc((1 - var(--workshop-c, 1)) * -26px)) scale(calc(.5 + .5 * var(--workshop-c, 1)));
}
#workshops.workshop-live .workshop-footer > * {
  opacity: var(--workshop-c, 1);
  transform: translateY(calc((1 - var(--workshop-c, 1)) * 34px)) scale(calc(.82 + .18 * var(--workshop-c, 1)));
}
#workshops.workshop-live .workshop-footer > :is(button, a) { box-shadow: 0 10px 26px #0000002e; }

/* Phone: the card pins under the header while the spacer scrolls past */
@media (max-width: 760px) {
  #workshops.workshop-live .workshop { position: sticky; top: var(--m-top, 120px); transition: top .3s var(--ease-ui); }
  #workshops.workshop-live .workshop-pin { height: 58svh; }
  #workshops.workshop-live .workshop-label { flex-direction: column; align-items: flex-start; flex-wrap: nowrap; gap: 6px; padding: 26px 24px 30px; }
  #workshops.workshop-live .workshop-label .large { font-size: 2.85rem; line-height: 1.15; }
}

/* Desktop: no pin, calmer scale */
@media (min-width: 761px) {
  #workshops.workshop-live .workshop-label .large {
    transform: translate3d(calc(var(--workshop-s) * ((1 - var(--workshop-a, 1)) * -60px + (var(--workshop-p, 1) - 1) * 30px)), 0, 0) scale(calc(1 + (1 - var(--workshop-a, 1)) * .2));
  }
  #workshops.workshop-live .workshop-label::after { width: 210px; height: 210px; }
}

/* sections/journal.css */
/* journal: search field that arrives with the head, rows that slide in one by one with a ghost number and a top rule.
   Owns: #journal .section-head .search, #posts and every .post in it (rebuilt by cms.js, no .post-meta on the live shape).
   Created nodes: none. State class: journal-live on #journal, added by script.
   Properties, each written on the element that reads it: --journal-h arrival of the search field (on .search), --journal-p arrival of a row (on each .post).
   --journal-s is +1 in RTL and -1 in LTR. --journal-d is the row's slide direction, --journal-press its pressed scale (both set here). Counter: journal-n.
   @external --ease-out, --green, --sage */
#journal.journal-live { --journal-s: 1; }
html[dir="ltr"] #journal.journal-live { --journal-s: -1; }

#journal.journal-live .section-head .search {
  transform-origin: calc(50% + var(--journal-s) * 50%) 50%;
  transform: translateY(calc((1 - var(--journal-h, 1)) * 26px)) scaleX(calc(.55 + .45 * var(--journal-h, 1)));
  opacity: var(--journal-h, 1);
}

#journal.journal-live #posts { position: relative; counter-reset: journal-n; }
#journal.journal-live #posts .post {
  --journal-d: 1;
  position: relative;
  isolation: isolate;
  counter-increment: journal-n;
  opacity: calc(var(--journal-p, 1) * 1.5);
  transform: translate3d(calc(var(--journal-s) * var(--journal-d) * (1 - var(--journal-p, 1)) * 76px), calc((1 - var(--journal-p, 1)) * 22px), 0) rotate(calc(var(--journal-d) * (1 - var(--journal-p, 1)) * 2.2deg));
  scale: var(--journal-press, 1);
  transition: scale .22s var(--ease-out), background-color .3s;
}
#journal.journal-live #posts .post:nth-child(even) { --journal-d: -1; }
#journal.journal-live #posts .post:active {
  --journal-press: .982;
  background-color: color-mix(in srgb, var(--sage) 38%, transparent);
}
#journal.journal-live #posts .post::before {
  content: counter(journal-n, decimal-leading-zero);
  position: absolute;
  z-index: -1;
  inset-inline-end: 4px;
  top: 10px;
  pointer-events: none;
  font: italic 4.4rem/1 Georgia, serif;
  color: transparent;
  -webkit-text-stroke: 1px color-mix(in srgb, var(--green) 70%, transparent);
  opacity: calc(var(--journal-p, 1) * .5);
  transform: translateY(calc((1 - var(--journal-p, 1)) * 46px)) scale(calc(.8 + .2 * var(--journal-p, 1)));
  transform-origin: 50% 50%;
}
#journal.journal-live #posts .post::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  top: -1px;
  height: 2px;
  background: var(--green);
  pointer-events: none;
  transform-origin: calc(50% + var(--journal-s) * 50%) 50%;
  transform: scaleX(calc(var(--journal-p, 1) * 1.4));
  opacity: calc(1 - max(0, (var(--journal-p, 1) - .7) / .3));
}
#journal.journal-live #posts .post > button { transition: transform .25s var(--ease-out), background-color .2s; }
#journal.journal-live #posts .post:active > button { transform: rotate(45deg) scale(.94); }

@media (max-width: 760px) {
  #journal.journal-live #posts .post { padding-inline-end: 4px; }
  #journal.journal-live #posts .post::before { inset-inline-end: 0; top: 6px; }
}
@media (min-width: 761px) {
  #journal.journal-live #posts .post::before { font-size: 3.6rem; inset-inline-end: 120px; top: 14px; }
}

/* sections/contact.css */
/* contact: the closing block. A dark panel grows from a small centred shape, a soft glow drifts in, then kicker, heading, text and button arrive in turn.
   Owns: #contact and everything inside it, plus the two layers below.
   Nodes created by contact.js (appended last, aria-hidden, never first so the cms.js lookup of the first div inside #contact is untouched): i.contact-bg (the panel, the only clipped layer), and inside it i.contact-glow (the two soft highlights; the panel's clip also clips it, so it needs none of its own).
   State classes on #contact, added by script: contact-live (turns the css on), contact-near (only while the block is near the screen: the button pulse runs then and not before).
   Properties, each written on the element that reads it: --contact-g panel grows (on bg), --contact-r raw 0..1 (on glow), --contact-k kicker, --contact-h heading, --contact-t text, --contact-b button.
   --contact-s is +1 in RTL and -1 in LTR. Every variable falls back to its final state.
   The h2 keeps only its fade (--contact-h), which ties it to the panel: its words (features/headings) start when the fade passes half. Its rise and blur went to the words.
   @external --feature-bg, --feature-text, --button-bg, --ease-out */
#contact.contact-live {
  --contact-s: 1;
  position: relative;
  isolation: isolate;
  background: transparent;
  color: var(--feature-text);
}
html[dir="ltr"] #contact.contact-live { --contact-s: -1; }

#contact.contact-live .contact-bg {
  position: absolute;
  z-index: -1;
  inset: 0;
  pointer-events: none;
  background: var(--feature-bg);
  clip-path: inset(calc((1 - var(--contact-g, 1)) * 38%) calc((1 - var(--contact-g, 1)) * 12%) round calc(12px + (1 - var(--contact-g, 1)) * 40px));
}
#contact.contact-live .contact-glow {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(55% 70% at calc(50% + var(--contact-s) * -30%) 0%, color-mix(in srgb, var(--feature-text) 18%, transparent), transparent 70%),
    radial-gradient(40% 50% at calc(50% + var(--contact-s) * 30%) 100%, color-mix(in srgb, var(--feature-text) 12%, transparent), transparent 70%);
  transform: translateY(calc((1 - var(--contact-r, 1)) * -60px));
}

#contact.contact-live .kicker {
  color: var(--feature-text);
  opacity: calc(var(--contact-k, 1) * .75);
  transform: translateY(calc((1 - var(--contact-k, 1)) * 20px));
}
#contact.contact-live h2 { opacity: var(--contact-h, 1); }
#contact.contact-live p {
  color: var(--feature-text);
  opacity: calc(var(--contact-t, 1) * .85);
  transform: translateY(calc((1 - var(--contact-t, 1)) * 30px));
}
#contact.contact-live .primary {
  position: relative;
  background: var(--feature-text);
  color: var(--feature-bg);
  font-weight: 600;
  opacity: calc(var(--contact-b, 1) * 2);
  transform: translateY(calc((1 - var(--contact-b, 1)) * 46px)) scale(calc(.55 + .45 * var(--contact-b, 1)));
  box-shadow: 0 14px 40px #00000045;
}
/* The soft pulse around the button, only while the block is near the screen */
#contact.contact-live .primary::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 2px solid var(--feature-text);
  pointer-events: none;
}
#contact.contact-near .primary::after { animation: contact-ring 2.4s var(--ease-out) infinite; }
@keyframes contact-ring {
  from { opacity: .7; transform: scale(1); }
  to { opacity: 0; transform: scale(1.2, 1.7); }
}

@media (max-width: 760px) {
  #contact.contact-live { margin-block: 48px; padding: 72px 0 80px; min-height: 66svh; justify-content: center; align-items: flex-start; gap: 30px; }
  #contact.contact-live .contact-bg { inset: -24px; transform-origin: 50% 50%; transform: scale(calc(.88 + .12 * var(--contact-g, 1))); }
  #contact.contact-live h2 { font-size: clamp(2.2rem, 9.4vw, 2.8rem); line-height: 1.5; }
  #contact.contact-live .primary { align-self: stretch; min-height: 58px; font-size: 1.0625rem; gap: 14px; }
}
@media (min-width: 761px) {
  #contact.contact-live { padding: 56px 56px; }
}

/* sections/footer.css */
/* footer: the sign-off. A thin rule draws in, the big name rises from behind its own baseline, the small lines fade up.
   Owns: the page footer and its children. Created nodes: none. State class: footer-live on footer, added by script.
   Property --footer-p (0..1), written on the footer (rule) and on each child that reads it. --footer-s is +1 in RTL and -1 in LTR. Falls back to the final state.
   @external --olive */
footer.footer-live {
  --footer-s: 1;
  position: relative;
  flex-wrap: wrap;
  row-gap: 6px;
  padding-block: 44px 36px;
}
html[dir="ltr"] footer.footer-live { --footer-s: -1; }
footer.footer-live::before {
  content: "";
  position: absolute;
  inset-inline: 0;
  top: 0;
  height: 1px;
  background: var(--olive);
  transform-origin: calc(50% + var(--footer-s) * 50%) 50%;
  transform: scaleX(var(--footer-p, 1));
  opacity: .5;
}
footer.footer-live .brand {
  order: -1;
  flex: 1 1 100%;
  display: block;
  padding-block: .14em;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: 0;
  font-size: clamp(4rem, 23vw, 6.2rem);
  clip-path: inset(calc((1 - var(--footer-p, 1)) * 100%) 0 0 0);
  transform: translateY(calc((1 - var(--footer-p, 1)) * 42%));
}
footer.footer-live > :not(.brand) { opacity: var(--footer-p, 1); }
footer.footer-live > span { transform: translateY(calc((1 - var(--footer-p, 1)) * 16px)); }

@media (min-width: 761px) {
  footer.footer-live .brand { font-size: clamp(5rem, 10vw, 9rem); }
}

/* features/cinema.css */
/* cinema: on a phone, as the portfolio row reaches the middle of the screen the page dims like a cinema and the centred card glows in its poster's colours.
   @external work-heading, work-grid, work-card, works-meta, works-progress, works-count, works-on, --feature-bg, --feature-text, --m-z-hud
   Node (appended to body by script): div.cinema-stage > div.cinema-dark + div.cinema-glow > two div.cinema-layer.
   State classes: .cinema-on (stage visible), .cinema-show (the poster layer in front), .cinema-lit (light text) on .work-heading, .work-grid, .works-meta, .works-progress.
   Values written by script: --cinema-k on the stage (0 lights up .. 1 dark), --cinema-y on the glow, --cinema-img on a layer. */
@property --cinema-k { syntax: '<number>'; inherits: false; initial-value: 0; }
@property --cinema-y { syntax: '<length>'; inherits: false; initial-value: 0px; }

.cinema-stage { display: none; }

@media (max-width: 760px) and (prefers-reduced-motion: no-preference) {
  html.motion body:not(.dashboard-mode) .cinema-stage {
    display: block;
    position: fixed;
    inset: 0;
    z-index: calc(var(--m-z-hud) - 1);
    overflow: hidden;
    pointer-events: none;
    visibility: hidden;
    opacity: var(--cinema-k);
  }
  html.motion body:not(.dashboard-mode) .cinema-stage.cinema-on { visibility: visible; }
  .cinema-dark { position: absolute; inset: 0; background: color-mix(in srgb, var(--feature-bg) 95%, transparent); }
  .cinema-glow { position: absolute; top: 0; left: 0; width: 100%; height: 0; transform: translateY(var(--cinema-y)); }
  .cinema-layer {
    position: absolute;
    left: 50%;
    top: 0;
    /* Blurred small and scaled up four times: the same look for a sixteenth of the pixels */
    width: 43.75vw;
    height: 43.75vw;
    margin: -21.875vw 0 0 -21.875vw;
    transform: scale(4);
    background: center / cover no-repeat var(--cinema-img, none);
    filter: blur(9.5px) saturate(1.8) brightness(1.25);
    -webkit-mask-image: radial-gradient(closest-side, #000 48%, transparent);
    mask-image: radial-gradient(closest-side, #000 48%, transparent);
    opacity: 0;
    transition: opacity .7s var(--ease-ui);
  }
  .cinema-layer.cinema-show { opacity: .95; }

  /* The row sits above the dark layer (and below the header, progress bar and dock) */
  html.motion body:not(.dashboard-mode) .work-heading,
  html.motion body:not(.dashboard-mode) .works-meta,
  html.motion body:not(.dashboard-mode) .works-progress { position: relative; }
  html.motion body:not(.dashboard-mode) :is(.work-heading, .work-grid, .works-meta, .works-progress) { z-index: var(--m-z-hud); }

  /* Light text on the dark scene */
  html.motion body:not(.dashboard-mode) .work-heading h3,
  html.motion body:not(.dashboard-mode) .work-heading p,
  html.motion body:not(.dashboard-mode) .works-count,
  html.motion body:not(.dashboard-mode) .works-count span,
  html.motion body:not(.dashboard-mode) .work-grid > .work-card h4 { transition: color .45s var(--ease-ui); }
  html.motion body:not(.dashboard-mode) .work-heading.cinema-lit :is(h3, p),
  html.motion body:not(.dashboard-mode) .works-meta.cinema-lit :is(.works-count, .works-count span),
  html.motion body:not(.dashboard-mode) .work-grid.cinema-lit > .work-card h4 { color: var(--feature-text); }
  html.motion body:not(.dashboard-mode) .works-progress.cinema-lit { background: color-mix(in srgb, var(--feature-text) 24%, transparent); }
}

/* features/dock.css */
/* dock: floating bottom navigation on phones (<=760px), see dock.js.
   Nodes (appended to body by script): div.dock-bar (holds i.dock-ind, a.dock-link x4, button.dock-contact) and, only during a flight, div.dock-fly.
   State classes: html.dock-on (the dock exists and the phone header link row is hidden), .dock-show, .dock-flying, .dock-snap, .dock-land on the bar, .dock-on on the active link.
   Values written by script on the element that uses them: --dock-ix, --dock-iw, --dock-io on i.dock-ind.
   @external --surface, --line, --ink, --muted, --olive, --sage, --button-bg, --button-text */

.dock-bar, .dock-fly { display: none; }

@media (max-width: 760px) {
  /* Without the script there is no dock and no html.dock-on: the header stays as it is. */
  html.dock-on header nav { display: none; }
  html.dock-on header .nav { min-height: 0; }
  /* Room under the footer so the dock never covers the last line of the page */
  html.dock-on body { padding-bottom: calc(88px + env(safe-area-inset-bottom)); }

  .dock-bar {
    display: flex;
    align-items: stretch;
    gap: 2px;
    position: fixed;
    z-index: 7;
    inset-inline: 0;
    bottom: calc(10px + env(safe-area-inset-bottom));
    width: min(calc(100% - 24px), 420px);
    height: 64px;
    margin-inline: auto;
    padding: 6px;
    box-sizing: border-box;
    border-radius: 32px;
    border: 1px solid color-mix(in srgb, var(--line) 80%, transparent);
    background: color-mix(in srgb, var(--surface) 78%, transparent);
    -webkit-backdrop-filter: blur(16px) saturate(1.5);
    backdrop-filter: blur(16px) saturate(1.5);
    box-shadow: 0 12px 32px color-mix(in srgb, var(--ink) 20%, transparent), 0 2px 6px color-mix(in srgb, var(--ink) 12%, transparent);
    opacity: 0;
    transform: translateY(calc(100% + 28px + env(safe-area-inset-bottom)));
    -webkit-user-select: none;
    user-select: none;
  }
  .dock-bar.dock-show { opacity: 1; transform: none; }

  .dock-ind {
    position: absolute;
    top: 6px;
    bottom: 6px;
    left: 0;
    width: var(--dock-iw, 0px);
    border-radius: 24px;
    background: var(--sage);
    opacity: var(--dock-io, 0);
    transform: translateX(var(--dock-ix, 0px));
    pointer-events: none;
  }

  .dock-link {
    position: relative;
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    border-radius: 24px;
    color: var(--muted);
    font-size: clamp(.5rem, 2.7vw, .6875rem);
    font-weight: 500;
    line-height: 1.2;
    white-space: nowrap;
    overflow: hidden;
    min-height: 44px;
  }
  .dock-link[hidden] { display: none; }
  .dock-link svg { width: 21px; height: 21px; flex: none; }
  .dock-link.dock-on { color: var(--olive); font-weight: 600; }
  html:not([lang="en"]) .dock-link { font-size: clamp(.5625rem, 3.1vw, .75rem); }
  html[lang="en"] .dock-link { letter-spacing: -.01em; }

  .dock-contact {
    position: relative;
    flex: 0 0 auto;
    min-width: 0;
    min-height: 44px;
    margin-inline-start: 4px;
    padding: 0 clamp(8px, 3vw, 13px);
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    border: 0;
    border-radius: 26px;
    background: var(--button-bg);
    color: var(--button-text);
    font: inherit;
    font-size: clamp(.6875rem, 3.1vw, .8125rem);
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    box-shadow: 0 4px 12px color-mix(in srgb, var(--button-bg) 45%, transparent);
  }
  .dock-contact svg { width: 20px; height: 20px; flex: none; }
  .dock-flying .dock-contact { opacity: 0; }

  /* The clone that flies between the hero button and the contact action */
  .dock-fly {
    display: grid;
    place-items: center;
    position: fixed;
    z-index: 8;
    left: 0;
    top: 0;
    box-sizing: border-box;
    margin: 0;
    border-radius: 14px;
    background: var(--button-bg);
    color: var(--button-text);
    font-size: .9375rem;
    line-height: 1.6;
    white-space: nowrap;
    overflow: hidden;
    pointer-events: none;
    transform-origin: 0 0;
    will-change: transform;
    box-shadow: 0 10px 26px color-mix(in srgb, var(--button-bg) 40%, transparent);
  }
}

@media (max-width: 760px) and (prefers-reduced-motion: no-preference) {
  .dock-bar {
    transition: transform .6s var(--ease-out), opacity .3s var(--ease-ui);
  }
  .dock-ind {
    transition: transform .5s var(--ease-out), opacity .3s var(--ease-ui);
  }
  .dock-bar.dock-snap .dock-ind { transition: none; }
  .dock-link, .dock-contact { transition: transform .18s var(--ease-ui), color .25s; }
  .dock-link:active, .dock-contact:active { transform: scale(.92); }
  .dock-link svg { transition: transform .45s var(--ease-out); }
  .dock-link.dock-on svg { transform: translateY(-1px) scale(1.12); }
  .dock-land .dock-contact { animation: dock-pop .5s var(--ease-out); }
  @keyframes dock-pop {
    0% { transform: scale(.78); }
    55% { transform: scale(1.09); }
    100% { transform: scale(1); }
  }
}

/* features/sheets.css */
/* sheets: on phones the contact, detail and profile dialogs become bottom sheets (see sheets.js).
   Everything is gated by html.sheets-ready, which the script adds, so without the script the dialogs stay as the site made them.
   Created nodes: div.sheets-grab (first child of each of the three dialogs).
   Classes: html.sheets-ready, html.sheets-open; dialog.sheets-leaving, dialog.sheets-dragging.
   Property: --sheets-p (0..1 drag progress, written on the dragged dialog, read by its backdrop). */

.sheets-grab { display: none; }

@media (max-width: 760px) {
  html.sheets-open { overflow: hidden; }

  html.sheets-ready body:not(.dashboard-mode) :is(#detail, #contactDialog, #profileDialog) {
    width: 100%;
    max-width: none;
    max-height: 88vh;
    max-height: 88dvh;
    margin: auto 0 0;
    padding: 0 22px calc(30px + env(safe-area-inset-bottom));
    border-width: 1px 0 0;
    border-radius: 26px 26px 0 0;
    overscroll-behavior: contain;
    /* the first shadow is a solid skirt so the spring overshoot never shows a gap under the sheet */
    box-shadow: 0 90px 0 0 var(--paper), 0 -10px 44px #00000026;
  }
  html.sheets-ready body:not(.dashboard-mode) #profileDialog { padding-inline: 18px; }
  html.sheets-ready body:not(.dashboard-mode) #profileFrame { height: max(280px, calc(88dvh - 270px)); }

  /* An input under 16px makes iOS zoom the page on focus */
  html.sheets-ready body:not(.dashboard-mode) #contactDialog :is(input, textarea) { font-size: 16px; scroll-margin-bottom: 40vh; }

  html.sheets-ready body:not(.dashboard-mode) :is(#detail, #contactDialog, #profileDialog) .sheets-grab {
    display: block;
    position: sticky;
    top: 0;
    z-index: 2;
    width: calc(100% + 44px);
    height: 36px;
    margin: 0 -22px 2px;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: var(--paper);
    touch-action: none;
    cursor: grab;
    -webkit-tap-highlight-color: transparent;
  }
  html.sheets-ready body:not(.dashboard-mode) #profileDialog .sheets-grab { width: calc(100% + 36px); margin-inline: -18px; }
  html.sheets-ready body:not(.dashboard-mode) .sheets-grab::before {
    content: "";
    position: absolute;
    inset: 11px 0 auto;
    width: 44px;
    height: 5px;
    margin-inline: auto;
    border-radius: 3px;
    background: var(--muted);
    opacity: .38;
  }

  html.sheets-ready body:not(.dashboard-mode) :is(#detail, #contactDialog, #profileDialog)::backdrop {
    opacity: calc(1 - var(--sheets-p, 0));
    transition: opacity .3s var(--ease-ui);
  }
  html.sheets-ready body:not(.dashboard-mode) :is(#detail, #contactDialog, #profileDialog).sheets-dragging::backdrop { transition: none; }
  html.sheets-ready body:not(.dashboard-mode) :is(#detail, #contactDialog, #profileDialog).sheets-leaving::backdrop { opacity: 0; transition: opacity .3s var(--ease-ui); }
  html.sheets-ready body:not(.dashboard-mode) :is(#detail, #contactDialog, #profileDialog).sheets-dragging { animation: none; }
}

/* Entrance: a spring rise, the backdrop fading in, the content following a beat behind */
@media (max-width: 760px) and (prefers-reduced-motion: no-preference) {
  html.motion.sheets-ready body:not(.dashboard-mode) :is(#detail, #contactDialog, #profileDialog)[open] {
    animation: sheets-rise .6s cubic-bezier(.2, .9, .25, 1) backwards;
    animation-timing-function: linear(0, .036, .124, .24, .367, .492, .608, .71, .796, .866, .92, .961, .991, 1.011, 1.023, 1.029, 1.031, 1.031, 1.028, 1.025, 1.021, 1.017, 1.013, 1.01, 1.007, 1.005, 1.003, 1.001, 1);
  }
  html.motion.sheets-ready body:not(.dashboard-mode) :is(#detail, #contactDialog, #profileDialog)[open]::backdrop {
    animation: sheets-fade .4s var(--ease-ui) backwards;
  }
  html.motion.sheets-ready body:not(.dashboard-mode) :is(#detail, #contactDialog, #profileDialog)[open] > :not(.sheets-grab, iframe) {
    animation: sheets-in .5s var(--ease-out) .12s backwards;
  }
  html.motion.sheets-ready body:not(.dashboard-mode) :is(#detail, #contactDialog, #profileDialog)[open] > :nth-child(n+4) { animation-delay: .19s; }
  html.motion.sheets-ready body:not(.dashboard-mode) :is(#detail, #contactDialog, #profileDialog)[open] > :nth-child(n+6) { animation-delay: .26s; }
  html.motion.sheets-ready body:not(.dashboard-mode) :is(#detail, #contactDialog, #profileDialog).sheets-leaving,
  html.motion.sheets-ready body:not(.dashboard-mode) :is(#detail, #contactDialog, #profileDialog).sheets-leaving > * { animation: none; }

  @keyframes sheets-rise { from { transform: translateY(100%); } }
  @keyframes sheets-fade { from { opacity: 0; } }
  @keyframes sheets-in { from { opacity: 0; transform: translateY(14px); } }
}

/* features/viewer.css */
/* Fullscreen reel viewer for the portfolio row (phones only). Built by viewer.js inside one <dialog class="viewer-dialog"> appended to <body>.
   @external work-card, work-grid
   Classes on the page: html.viewer-on (phone width, viewer ready), html.viewer-open (viewer showing), .viewer-lifted (the card the viewer grew from).
   Inside the dialog: .viewer-shade, .viewer-stage > .viewer-pager > .viewer-slide (.viewer-bg, .viewer-frame > .viewer-video + .viewer-spin, .viewer-caption > .viewer-title),
   .viewer-stage > .viewer-ui (.viewer-bars > .viewer-seg > i, .viewer-row > .viewer-count + .viewer-tools > .viewer-btn), .viewer-glyph, .viewer-hint.
   Properties: --viewer-fw --viewer-fh (frame size, on each frame), --viewer-p (progress, on each segment fill), --viewer-fade (shade). */

/* Page while the viewer is open: nothing behind it moves */
html.viewer-open { overflow: hidden; overscroll-behavior: none; }

/* Phone row: the card is the button, the native player controls step aside (the script sets .viewer-on and removes the attribute) */
html.viewer-on .work-card { cursor: pointer; -webkit-tap-highlight-color: transparent; }
html.viewer-on .work-card video { pointer-events: none; }
html.viewer-on .work-card video::-webkit-media-controls { display: none !important; }
html.viewer-on .work-card video::-webkit-media-controls-start-playback-button { display: none !important; }
html.viewer-on .work-card:focus-visible { outline: 2px solid var(--green); outline-offset: 6px; border-radius: 14px; }
html.viewer-on .work-card.viewer-lifted > video { visibility: hidden; }

/* Container: a full-screen modal in the top layer with its own black surface */
dialog.viewer-dialog {
  position: fixed; inset: 0; margin: 0; padding: 0; border: 0; border-radius: 0;
  width: 100%; height: 100%; max-width: none; max-height: none;
  background: transparent; color: #fff; box-shadow: none; overflow: hidden;
  touch-action: none; overscroll-behavior: none; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
dialog.viewer-dialog::backdrop { background: transparent; }
html body:not(.dashboard-mode) dialog.viewer-dialog[open] { animation: none; }
html body:not(.dashboard-mode) dialog.viewer-dialog[open]::backdrop { animation: none; }

.viewer-shade { position: absolute; inset: 0; background: #000; opacity: var(--viewer-fade, 1); }
.viewer-stage { position: absolute; inset: 0; overflow: hidden; transform-origin: 50% 50%; }
.viewer-pager { position: absolute; inset: 0; overflow: hidden; }
.viewer-slide { position: absolute; inset: 0; will-change: transform; }

/* The poster's colours, blurred, fill the letterbox */
/* Blurred small and scaled up four times: the same look for a sixteenth of the pixels */
.viewer-bg { position: absolute; left: 34%; top: 34%; width: 32%; height: 32%; transform: scale(4); object-fit: cover; filter: blur(5.5px) saturate(1.6) brightness(.6); pointer-events: none; }

/* The 9:16 picture; this is the element that grows out of the card and shrinks back into it */
.viewer-frame { position: absolute; inset: 0; margin: auto; width: var(--viewer-fw, 100%); height: var(--viewer-fh, 100%); overflow: hidden; background: #000; transform-origin: 50% 50%; will-change: transform; }
.viewer-video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; background: #000; pointer-events: none; }

.viewer-spin { position: absolute; inset: 0; margin: auto; width: 38px; height: 38px; border-radius: 50%; border: 3px solid color-mix(in srgb, #fff 28%, transparent); border-top-color: #fff; opacity: 0; pointer-events: none; }

/* Title at the foot of each page */
.viewer-caption { position: absolute; inset: auto 0 0; padding: 90px 22px calc(26px + env(safe-area-inset-bottom)); background: linear-gradient(to top, color-mix(in srgb, #000 78%, transparent), transparent); pointer-events: none; }
.viewer-title { margin: 0; font-size: 1.1875rem; font-weight: 600; line-height: 1.6; color: #fff; text-shadow: 0 1px 14px color-mix(in srgb, #000 60%, transparent); }

/* Top: segmented progress, counter, controls */
.viewer-ui { position: absolute; inset: 0 0 auto; padding: calc(10px + env(safe-area-inset-top)) 14px 34px; background: linear-gradient(to bottom, color-mix(in srgb, #000 62%, transparent), transparent); pointer-events: none; }
.viewer-bars { display: flex; gap: 3px; direction: inherit; }
.viewer-seg { flex: 1 1 0; height: 3px; border-radius: 3px; overflow: hidden; background: color-mix(in srgb, #fff 30%, transparent); }
.viewer-seg > i { display: block; width: 100%; height: 100%; border-radius: 3px; background: #fff; transform: scaleX(var(--viewer-p, 0)); transform-origin: right; }
html[dir="ltr"] .viewer-seg > i { transform-origin: left; }
.viewer-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px; }
.viewer-count { direction: ltr; font-size: .9375rem; font-weight: 600; letter-spacing: .04em; font-variant-numeric: tabular-nums; color: #fff; padding: 0 6px; text-shadow: 0 1px 8px color-mix(in srgb, #000 60%, transparent); }
.viewer-count em { font-style: normal; font-weight: 500; opacity: .62; }
.viewer-tools { display: flex; gap: 10px; }
.viewer-btn { display: grid; place-items: center; width: 44px; height: 44px; padding: 0; border: 0; border-radius: 50%; color: #fff; background: color-mix(in srgb, #000 40%, transparent); cursor: pointer; pointer-events: auto; -webkit-tap-highlight-color: transparent; }
.viewer-btn svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.viewer-btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

/* Play or pause mark in the middle */
.viewer-glyph { position: absolute; inset: 0; margin: auto; display: grid; place-items: center; width: 88px; height: 88px; border-radius: 50%; background: color-mix(in srgb, #000 48%, transparent); color: #fff; opacity: 0; pointer-events: none; }
.viewer-glyph svg { width: 40px; height: 40px; fill: currentColor; }
.viewer-glyph.viewer-keep { opacity: 1; }

/* One-time hint */
.viewer-hint { position: absolute; inset: auto 0 calc(118px + env(safe-area-inset-bottom)); margin: 0 auto; width: max-content; max-width: calc(100% - 40px); display: flex; align-items: center; gap: 10px; padding: 10px 18px; border-radius: 999px; font-size: .9375rem; font-weight: 500; color: #fff; background: color-mix(in srgb, #000 55%, transparent); opacity: 0; pointer-events: none; }
.viewer-hint svg { width: 20px; height: 20px; flex: none; fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.viewer-hint.viewer-show { opacity: 1; }

/* Holding the screen hides the chrome */
.viewer-held .viewer-ui, .viewer-held .viewer-caption { opacity: 0; }

@media (prefers-reduced-motion: no-preference) {
  html.motion .viewer-slide.viewer-snap { transition: transform var(--viewer-ms, .42s) cubic-bezier(.22, 1, .36, 1); }
  html.motion .viewer-ui, html.motion .viewer-caption { transition: opacity .25s var(--ease-ui); }
  html.motion .viewer-hint { transition: opacity .4s var(--ease-ui); }
  html.motion .viewer-glyph.viewer-keep { transition: opacity .18s var(--ease-ui); }
  html.motion .viewer-glyph.viewer-pop { animation: viewer-pop .7s var(--ease-out) both; }
  @keyframes viewer-pop { 0% { opacity: 0; transform: scale(.7); } 22% { opacity: 1; transform: scale(1); } 100% { opacity: 0; transform: scale(1.18); } }
  html.motion .viewer-slide.viewer-wait .viewer-spin { animation: viewer-spin .8s linear infinite, viewer-appear .3s .5s both; }
  @keyframes viewer-spin { to { transform: rotate(360deg); } }
  @keyframes viewer-appear { to { opacity: 1; } }
  html.motion .viewer-hint.viewer-show svg { animation: viewer-bob 1.1s ease-in-out infinite; }
  @keyframes viewer-bob { 0%, 100% { transform: translateY(2px); } 50% { transform: translateY(-4px); } }
}
@media (prefers-reduced-motion: reduce) {
  .viewer-slide.viewer-wait .viewer-spin { opacity: 1; }
  .viewer-glyph.viewer-pop { opacity: 1; }
}

/* features/theme.css */
/* theme: the new theme grows as a circle from the theme button (View Transitions API), see theme.js.
   Owns only the two root snapshots, and only while html.theme-vt is set by script. Created nodes: none. */
html.theme-vt::view-transition-old(root),
html.theme-vt::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}
html.theme-vt::view-transition-old(root) { z-index: 1; }
html.theme-vt::view-transition-new(root) { z-index: 2; }

/* features/haptics.css */
/* haptics: no visual output. The unit only calls navigator.vibrate (see haptics.js). Kept as an empty file so the manifest stays uniform. */

/* features/headings.css */
/* headings: word by word entrance for the hero h1 and the section h2s (#about, #services, #workshops, #journal, #contact).
   Owns: the word elements inside those headings, and the hero h1's visibility while html.pre holds the page back. The headings' own transform/opacity stay with their sections.
   Nodes created by headings.js, inside each heading while it plays: span.headings-w (clip box, one per word) > span.headings-i (the word that moves). Removed again when the entrance ends.
   State classes on the heading, added by script (the page is complete without them): headings-split, headings-armed (words waiting, hidden), headings-go (words playing).
   Properties: --headings-s +1 RTL / -1 LTR · --headings-step stagger · --headings-base first delay · --headings-i word index (on each word) · --headings-e time already elapsed when a playing heading is re-split (on each word).
   @external --ease-out */
@media (prefers-reduced-motion: no-preference) {
  /* The hero h1 is held back while the page is, so it never shows before its words are armed */
  html.motion.pre body:not(.dashboard-mode) .hero h1 { visibility: hidden; }

  html.motion body:not(.dashboard-mode) .headings-split {
    --headings-s: 1;
    --headings-step: 62ms;
    --headings-base: 60ms;
  }
  html.motion[dir="ltr"] body:not(.dashboard-mode) .headings-split { --headings-s: -1; }
  html.motion body:not(.dashboard-mode) .hero h1.headings-split { --headings-step: 80ms; --headings-base: 120ms; }

  /* Each word has a clip box taller than its line (room for Arabic marks above and below); the margins cancel the padding, so the line keeps its height */
  html.motion body:not(.dashboard-mode) .headings-split .headings-w {
    display: inline-block;
    padding: .4em .18em .46em;
    margin: -.4em -.18em -.46em;
    clip-path: inset(0);
  }
  html.motion body:not(.dashboard-mode) .headings-split .headings-i {
    display: inline-block;
    transform-origin: calc(50% + var(--headings-s) * 50%) 100%;
  }
  html.motion body:not(.dashboard-mode) .headings-armed .headings-i {
    opacity: 0;
    transform: translateY(calc(100% + .5em));
  }
  html.motion body:not(.dashboard-mode) .headings-go .headings-i {
    animation: headings-rise 1.05s var(--ease-out) both;
    animation-delay: calc(var(--headings-base) + var(--headings-i, 0) * var(--headings-step) - var(--headings-e, 0ms));
  }
  @keyframes headings-rise {
    from { opacity: 0; transform: translateY(calc(100% + .5em)) rotate(calc(var(--headings-s) * 6deg)); filter: blur(7px); }
    35% { opacity: 1; }
    to { opacity: 1; transform: none; filter: none; }
  }
}
