/* ==========================================================================
   Design tokens — the only place values live.
   Change a value here and it restyles the whole site.
   Shadow and motion are still placeholders flagged for iteration — they are
   named so they can be swapped without hunting. The accent is final.
   ========================================================================== */

:root {
  /* --- Color (palette E2, Oct 2026) --------------------------------------
     Built from the hero gradient so the page reads as one family with it:
     cool neutrals tinted toward its blue-violet end (hue ~288), one working
     colour for interaction taken from its blue, a violet band from its middle,
     and the covers in its own rendered colours. Chosen in the palette lab
     (site/_palette/, dev-only), where every value below was measured against
     every ground it can land on. Re-run the contrast-audit skill after any
     change here, and after adding any ground. */
  --color-dark: #1b1a26;      /* body ink — 17.2 on white */
  /* Labels, captions, meta. Cool slate, clears 4.5 on every ground text
     lands on: white 5.88, the light-blue section 5.31, the neutral band 5.31,
     the card hover fill 4.87. */
  --color-mid: #64627a;
  --color-light: #ffffff;     /* the page: white, as the hero and contact */
  /* The one interactive colour on a light ground: the focus ring and accent
     text. The hero's own blue (OKLCH hue 280, chroma 0.19 — the gradient's),
     darkened to read as text: 7.33 on white, 6.61 on the section ground. */
  --color-accent: #4d42c1;

  /* The hero's coral. Rare on purpose: the logo's hover wipe on light grounds
     (white glyph on it: 3.24) and nothing else, so the blue stays the one
     working colour. Links on the band use --on-band-accent, not this. */
  --color-highlight: #fa4c63;

  /* Derived, so the values above stay the single source of truth */
  --color-text: var(--color-dark);
  --color-bg: var(--color-light);
  --color-muted: var(--color-mid);

  /* Links inside content — prose, "View case study", "Next project", the back
     link, the footer's contact links — carry the blue at rest, so a little of
     the hero runs through every page and a link reads as one before it is
     hovered. Navigation, headings, cards and buttons stay ink. Hover goes one
     step deeper: 9.04 on white. On the band links stay the band's ink and turn
     --on-band-accent on hover. */
  --link-ink: var(--color-accent);
  --link-hover: #4137a6;

  /* The focus ring, by role rather than by reusing whichever colour happened
     to be the accent. */
  --focus-ring: var(--color-accent);
  --focus-ring-on-dark: var(--on-band-accent);

  /* Hairlines — every bordered table, card frame and section rule. Neutral,
     not tinted: next to other brands' screenshots in the case studies a tinted
     border read as a clash. --color-line-strong stays derived from it. */
  --color-line: #dadade;
  --color-line-strong: color-mix(in srgb, var(--color-line) 55%, var(--color-dark));
  --color-surface: color-mix(in srgb, var(--color-dark) 4%, var(--color-light));
  --color-plate: #ffffff;     /* plate behind UI screenshots */
  /* The small filled plates — project tags. An alpha of the ink rather than a
     flat hex, so a chip reads at the same strength on every ground (white,
     the light-blue section, the neutral band) without a hue clash. */
  --color-chip: color-mix(in srgb, var(--color-dark) 10%, transparent);

  /* Plate for screenshots exported with a TRANSPARENT background: the
     case-study band ground. Tinted bands flip it to the page (case-study.css),
     so a screenshot always sits on the opposite of its band. */
  --figure-tint: var(--ground-band);
  /* A surface, so --radius-lg — the same corner as every other plate. */
  --figure-tint-radius: var(--radius-lg);

  /* The covers: one flat colour per project, the hero gradient's own rendered
     colours at 80% (mixed toward white so the cards step back behind the
     hero), assigned so the three on the home page are far apart on the wheel
     — coral, violet, blue, in card order, so the last one leads into the
     About section's deeper blue — with orchid and indigo on the projects
     page, in that order: stacked in one column no two neighbours are close
     in hue, and in two columns the only close pair is blue above indigo.
     Coral is on the security scanner because it reads as an alert.
     The card frames and the case-study cover read --cover-ground; site.css
     points it at the project's colour by link (the cards) or by the page's
     data-project (the case study). The default is for anything unassigned. */
  --cover-connect-domain: #b390d6;           /* violet */
  --cover-ai-security-scanner: #eb8199;      /* coral  */
  --cover-approval-workflows: #8c8af4;       /* blue   */
  --cover-batch-combined-payments: #d589b2;  /* orchid */
  --cover-recurring-payments: #a490e3;       /* indigo */
  --cover-ground: var(--ground-band);

  /* Every project cover is exported at this ratio (2400x1462, and the same
     shape at 1200 and 1000 wide). Both card frames name it rather than
     repeating the numbers, so a future re-export is one edit — and so the
     frames cannot drift apart from each other. The images fill these frames
     exactly, which is why `object-fit: cover` now crops nothing. */
  --cover-aspect: 2400 / 1462;

  --color-overlay: color-mix(in srgb, var(--color-dark) 88%, #000); /* lightbox */

  /* --- Section grounds --------------------------------------------------
     Everything that touches the gradient — the hero at the top, the footer's
     band at the bottom — stands on white, which flatters it most. The page is
     arranged so the section above the footer is never white: home's violet
     band, About's light-blue last band, the case studies' last band (they
     alternate counted from the end). The projects page is one white page.

     The light blue is the home and About section ground. The case studies
     have their own, near-neutral: behind other brands' screenshots the light
     blue competed. Same step from white either way (1.11). */
  --ground-warm: #f3f2fe;     /* the light-blue section ground */
  --ground-band: #f3f3f6;     /* case-study bands */
  --ground: var(--color-light);  /* contact */
  --ground-white: #ffffff;
  /* The About band: violet, from the gradient's middle, lifted to L 0.42 so it
     reads as colour rather than as dark. White on it 8.84, the muted ink
     6.50, the band's link colour 5.42. */
  --ground-ink: #4e32a2;
  /* Links and the focus ring on the band. Coral is 2.7 on it and the blue
     vanishes (1.8), so the band has its own: a pale pink, 5.42. */
  --on-band-accent: #ffb3c1;

  /* Where the About ground starts as the section scrolls in — see the
     transition in hero.css. Only the frozen v0 home uses it. */
  --about-bg-from: #4e32a2;

  /* --- Hero ink --------------------------------------------------------- */
  /* The hero stands on the page ground now — no photo, no scrim — so its ink is
     simply the site's ink. Kept as its own token rather than inlined, because
     the hero is still the one section that could take a ground of its own. */
  --hero-ink: var(--color-dark);
  --hero-ink-muted: color-mix(in srgb, var(--hero-ink) 68%, transparent);

  /* Text over the violet About band: 8.84 and, at 82%, 6.50. Re-check the
     muted step if --ground-ink gets lighter — it fell under 4.5 on mid-tone
     grounds before. */
  --ink-on-dark: #fdfbf9;
  --ink-on-dark-muted: color-mix(in srgb, var(--ink-on-dark) 82%, transparent);

  /* --- Hero recede (the pinned hero → About transition) ------------------ */
  /* Where the hero ends up once About has covered it. Progress is how far
     About's top edge has travelled from the bottom of the viewport to the top,
     so these are the values at full cover — see hero.css.
     parallax.js mirrors the same numbers for browsers without scroll-driven
     animations; --hero-recede-ease-end lives only there, because a keyframe
     percentage can't be a custom property. Keep the two in step.

     There is no opacity token here on purpose — a fade washed the hero toward
     the page ground rather than reading as depth. Nor a scale any more: the
     push-in belonged to the photo layer, which is gone. Blur alone now. */
  --hero-recede-blur-text: 12px;    /* the lockup, as the panel covers it */

  /* --- Type ------------------------------------------------------------- */
  --font-sans: "DM Sans", ui-sans-serif, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
  --font-mono: "DM Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;

  --fw-regular: 400;
  --fw-medium: 500;
  --fw-semibold: 600;
  --fw-bold: 700;

  /* --- The type scale (Oct 2026) -----------------------------------------
     Seven sans steps. Every step grows between the same two window widths —
     390 and 1440 — and is held beyond them, so the steps move together and can
     never cross or close up: the order on a phone is the order at 1920. Before
     this, each size had its own fluid formula, so two of them could be far
     apart at one width and nearly equal at another (the home section heading
     was larger than the hero at 1440 and smaller at 1920).

     Two ratios: x1.13 between neighbours at 390, x1.2083 at 1440. The desktop
     ratio is set so the top step lands exactly on 56px, the case-study title
     size; the phone ratio so that step is 33px on a phone, as before. Step n
     is 16 x 1.13^n at 390 and 18 x 1.2083^n at 1440. The values are generated
     — change a base or a ratio and regenerate all seven, never one by hand.

     The role tokens below point at steps; components read the roles. The type
     lab (site/_type/, dev-only) shows every role on the scale at every width. */
  --type-0: clamp(1rem, 0.9536rem + 0.1905vw, 1.125rem);         /* 16.0 -> 18.0 */
  --type-1: clamp(1.13rem, 1.0448rem + 0.3494vw, 1.3593rem);     /* 18.1 -> 21.7 */
  --type-2: clamp(1.2769rem, 1.1412rem + 0.5568vw, 1.6423rem);   /* 20.4 -> 26.3 */
  --type-3: clamp(1.4429rem, 1.2418rem + 0.825vw, 1.9843rem);    /* 23.1 -> 31.7 */
  --type-4: clamp(1.6305rem, 1.3456rem + 1.1688vw, 2.3975rem);   /* 26.1 -> 38.4 */
  --type-5: clamp(1.8424rem, 1.4508rem + 1.6066vw, 2.8968rem);   /* 29.5 -> 46.3 */
  --type-6: clamp(2.082rem, 1.5552rem + 2.1608vw, 3.5rem);       /* 33.3 -> 56.0 */

  /* Mono: two sizes, and they do not scale. Every label — section labels,
     captions, meta lines, nav — is 15. 14 was too easy to miss in the hero.
     Tags are a step under, at 13, and keep their plate: a plate is what tells
     a tag apart from the meta line above it, which is the same font, colour
     and separator. Kept as separate names because they are separate roles,
     free to move apart again. */
  --fs-label: 0.9375rem;
  --fs-caption: var(--fs-label);
  --fs-meta: var(--fs-label);
  --fs-nav: var(--fs-label);

  /* Nav pill geometry. The pill's height is measured from the link at runtime
     rather than pinned at the component's 30px, so it stays correct if the
     label size ever changes. */
  --nav-pad-x: 0.75rem;
  --nav-pad-y: 0.375rem;
  /* The active-tab underline. The body ink, not the accent: the label beside
     it is marked by weight now, so the bar is the shape that says "current"
     and it says it in the same ink the labels are written in. Its dark-ground
     counterpart is --ink-on-dark, set in site.css — black is invisible on the
     wine band. */
  --nav-bar-ink: var(--color-dark);
  /* Nav hover. Derived from the ink rather than a new standalone colour —
     the accent is unusable behind a 70% header, see site.css. */
  --nav-hover-ink: color-mix(in srgb, var(--color-dark) 88%, #000);
  /* Sans roles, each on a step. */
  --fs-body: var(--type-0);         /* body prose, "View case study", buttons */
  --fs-small: var(--type-0);        /* fact values, table text, one-liners */
  --fs-lead: var(--type-1);         /* case-study intros, card one-liners */
  --fs-h3: var(--type-1);           /* table names, "What I learned" titles */
  --fs-card-name: var(--type-2);    /* projects-page card names */
  --fs-card-title: var(--type-3);   /* home card names */
  --fs-about-lead: var(--type-3);   /* the About paragraph */
  --fs-h2: var(--type-4);           /* case-study section headlines */
  --fs-display: var(--type-4);      /* the home section heading */
  --fs-h1: var(--type-6);           /* case-study titles, contact headline */
  --fs-tag: 0.8125rem;              /* see the mono note above */

  --lh-tight: 1.02;
  --lh-snug: 1.14;
  --lh-body: 1.6;
  --ls-tight: -0.03em;
  --ls-normal: -0.011em;
  --ls-wide: 0.08em;
  /* Tracking for the label role. 0.08em was opening up uppercase; on sentence
     case it reads as broken spacing, so labels take a normal letterfit. */
  --ls-label: 0.01em;

  /* --- Spacing (4px base) ----------------------------------------------- */
  --sp-1: 0.25rem;
  --sp-2: 0.5rem;
  --sp-3: 0.75rem;
  --sp-4: 1rem;
  --sp-5: 1.25rem;
  --sp-6: 1.5rem;
  --sp-8: 2rem;
  --sp-10: 2.5rem;
  --sp-12: 3rem;
  --sp-16: 4rem;
  --sp-20: 5rem;
  --sp-24: 6rem;
  --sp-32: 8rem;

  /* --- The spacing scale ------------------------------------------------
     Five values, all multiples of 8, and nothing between them:

       16  --sp-4       tight — gaps inside one text block
       24  --sp-6       component padding; image to its caption
       40  --sp-10      between items, compact (small screens)
       56  --space-56   between items, roomy (desktop); between sections;
                        above a CTA — at every width
       80  --sp-20      between major sections, long-form case studies only

     56 is the one step with no --sp- name, because the ramp doubles past
     --sp-12 (48) straight to --sp-16 (64). Named here so the scale can be
     spelled the same way everywhere it is used. */
  --space-56: 3.5rem;

  /* The gap above a section's closing CTA, and above the "Designed at" label
     inside it. One value on purpose: those two spaces are meant to match, and
     two tokens would drift apart the first time either moved.

     Flat 56 at every width. It used to be derived — one row gap plus a bit,
     which resolved to 60 on desktop and 44 below 62rem — and that put the
     three CTAs on the site on two different numbers, neither of them on the
     scale. Do not make it responsive again: 56 IS the between-sections step,
     and it is the same on a phone. */
  --space-cta: var(--space-56);

  /* Airy by default — sections breathe more than a dense layout would */
  --space-section: clamp(4.5rem, 10vw, 9rem);
  --space-block: clamp(2rem, 4.5vw, 3.5rem);
  /* Case-study sections have no dividers — the gap alone marks the break,
     so it has to be bigger than a normal section gap to read as one. */
  --space-section-lg: clamp(7rem, 15vw, 13rem);

  /* Case-study bands. Each section is a coloured band with this much padding
     top and bottom, so two adjacent bands put twice this between their content
     and the colour change, not a gap, is what marks the break.
     Fluid: 40px on a phone (80px between blocks), 80px on desktop (160px). */
  --cs-band-pad: clamp(2.5rem, 5.5vw, 5rem);

  /* The About page's own band rhythm. The case-study curve scaled by exactly
     1.5 — same shape, same breakpoint where it tops out (~1455px), just
     bigger: 60px on a phone, 120px on a desktop, so the space where the
     ground changes reads 120 narrow and 240 wide. Separate from
     --cs-band-pad on purpose; that one is shared by five case studies. */
  --about-band-pad: clamp(3.75rem, 8.25vw, 7.5rem);

  /* --- Footer gradient band -----------------------------------------------
     Not a corner accent: a band across the whole bottom of the footer that
     steps UP on the right, the same stepped profile the identity shape uses.
     The gradient fills it; the footer's ground is cut away above it.

     Two heights and one width: --fc-h is the band on the left, --fc-h2 the
     raised part on the right, --fc-w how far in from the right edge that
     raised part reaches. Proportions taken from the annotated mock — roughly
     13.5vw / 22vw / 23vw — with both ends pinned so it neither disappears on
     a phone nor runs away on a wide monitor. */
  --fc-h: clamp(4.5rem, 13.5vw, 11rem);
  /* The strip of ground beside the raised step — the footer's own closing
     area, which the closing row fills and centres itself in. It is the value
     worth setting directly, so the raised height is derived FROM it rather
     than the other way round; before this it was whatever
     --fc-h2 minus --fc-h happened to come to (112px at desktop).
     48px on a phone, 70px from ~1430px up. */
  --fc-strip: clamp(3rem, 4.9vw, 4.375rem);
  --fc-h2: calc(var(--fc-h) + var(--fc-strip));
  /* 5.5rem floor, not 8: at 375 an 8rem step began at x=247 while the
     copyright ran to 260, so the text sat over the raised gradient. */
  --fc-w: clamp(5.5rem, 23vw, 26rem);
  /* Bigger element, bigger radius — still the --radius-sm / -lg family, scaled. */
  --fc-r: 12px;

  /* --- Layout ----------------------------------------------------------- */
  /* Fluid, not a centred column: the logomark and every left-aligned label
     are measured from the browser edge, so they stay put as the window grows.
     Set a length here (e.g. 1600px) to cap the width on ultra-wide screens. */
  --container: none;
  /* Prose measure. NOT expressed in ch: `ch` is the width of the zero glyph,
     which in DM Sans is much wider than the average letter — the old `62ch`
     rendered as 106 characters a line, twice what it claimed. 31.5rem is 70
     characters at --fs-body, measured. */
  --measure-prose: 31.5rem;
  --container-text: var(--measure-prose);

  /* The content grid every page's copy sits on. Case-study sections and the
     About page both lay out on these, so keeping the numbers here is what
     stops the two drifting apart. */
  --grid-gutter: 32px;
  --grid-row-gap: 40px;

  /* 40px from the browser edge on desktop, easing down on small phones
     where 80px of a 375px screen would be a fifth of the viewport. */
  --gutter: clamp(1.25rem, 4vw, 2.5rem);
  --header-h: 4.5rem;

  /* THE vertical line. Every page hangs off it: nav, headlines, body copy,
     project cards and figures all start here, while the narrow rail to its
     left holds the logomark, section labels, year ranges and the arrows.
     One number moves the whole site.

     It is one column of a 5-column grid laid inside the gutters — so the
     content line is always a column in from the 40px edge, and it scales with
     the window instead of stalling at a fixed pixel width. Percentages here
     resolve against the container's content box, which is the same box for
     both uses (a grid column, and a margin-left offset).

     Below 62rem the rail collapses and everything aligns to the logomark
     instead. (Custom properties can't be used in media queries, so that
     breakpoint is written literally — search for "62rem" to change it.) */
  --rail: 20%;

  /* The page grid (V1c): a centred 1200px container with the site gutter
     (40px on desktop) inside it, so content starts at x=160 on a 1440 window.
     Born on the home page; the inner pages' <main> uses it too (pages.css).
     The header deliberately does NOT — its logo and links sit at the page
     edges on every page. --rail above is now only the frozen v0 home's and
     the footers'. */
  --home-max: 75rem;

  /* --- Corners ---------------------------------------------------------- */
  /* Two steps, one softness. How round a corner reads depends on the radius
     relative to the element, not on the pixel count: the hero shape renders
     at ~9px on a 168px height (~5%). One flat 4px made the small things
     (logomark 28px, tags 26px) rounder than that and the large ones (a 577px
     project card) nearly square — so the cards, not the logo, were what
     disagreed with the hero.

     -sm  chrome and controls: logomark, tags, buttons, nav, chips, small logos.
     -lg  surfaces: project cards and covers, figures and screenshots, tables,
          the facts strip, the lightbox image. (The v0-only .workplaces and
          .gallery-card__media stay -sm so the frozen home renders as shipped.)
     Pick by size, not by component family. The footer band's --fc-r (12px)
     is the same rule one step further up.

     The tablet identity-shape SVGs have 4px baked in (see --ident-shape) and
     sit beside the logomark, so they follow -sm. */
  --radius-sm: 4px;
  --radius-lg: 8px;
  /* Legacy alias, kept only so the untracked dev labs (site/_*) don't lose
     their corners. Shipped CSS uses -sm or -lg, never this. */
  --radius: var(--radius-sm);
  --radius-round: 50%;      /* exception: profile photo */
  --radius-pill: 999px;     /* The project tags used to be the reason this
                               exists; they take --radius-sm now, like every other
                               control on the site. Its one remaining consumer is
                               .lightbox__hint, a floating overlay label, where
                               a pill is right. */

  /* --- Shadow (soft, symmetric, no directional offset) ------------------- */
  --shadow-soft: 0 0 24px color-mix(in srgb, var(--color-dark) 7%, transparent);
  --shadow-lift: 0 0 40px color-mix(in srgb, var(--color-dark) 11%, transparent);

  /* --- Identity shape --------------------------------------------------- */
  /* The negative-space mark that sits inside the hero lockup and will later
     grow into the About background. Geometry and fill are separate on purpose:
     the mask defines the shape once, at any scale, and the fill is whatever is
     layered behind it.

     The SVG is authoritative — two rounded rects unioned, 4px rx baked in — so
     nothing here re-approximates the outline. The numbers below are read off
     its viewBox (395 × 147) and the step between its two rects (395 − 360 =
     35), which is what lets the type tuck into the notch. */
  /* Two assets, same outline at two proportions. The wide one cannot fit the
     lockup on one line below ~433px, so a narrower one takes over — the height
     stays two lines either way, only the width changes.

     The corner radius is NOT 4 in either file, on purpose. It is baked into
     the geometry, so it scales with the art: the wide shape renders at 253px
     from a 395 viewBox (×0.64), the narrow one at 84px from 206 (×0.41). The
     rx values below are pre-divided by those factors so both land on 4 CSS px
     at the size they are actually used — matching --radius-sm, the corner of the
     logomark and the other small chrome beside them. */
  --ident-shape: url("../images/hero/identity-shape.svg");      /* rx 6.25 */
  --ident-shape-sm: url("../images/hero/identity-shape-sm.svg"); /* rx 9.78 */
  --ident-w: 395;
  --ident-h: 147;
  --ident-step: 35;

  /* Fills. Phase 1 shows one; this is deliberately the first of a list rather
     than a single value, so adding a rotation later is more items plus a timer
     — not a restructure. */
  --ident-fill-1: url("../images/hero/identity-fill-01.webp");
  /* How far the fill is scaled past the shape. At plain `cover` the whole
     gradient sat inside the outline with nothing spare; this crops to the
     middle and lets the rest bleed out under the mask.

     A unitless ratio, applied as a transform on top of `cover` — NOT as
     `background-size: 165% auto`. That earlier form scaled by width only, which
     covered the height purely because the wide shape's aspect (2.687) happens
     to match the art's (2.68). The moment the narrow shape arrived at 1.401 the
     fill fell 8px short vertically, leaving the top and bottom 4px of the shape
     unpainted — and since the corner radius is 4px, that erased every rounded
     corner while leaving the notch intact. `cover` guarantees coverage at any
     aspect; the scale adds the bleed on top. */
  --ident-fill-zoom: 1.65;
  --ident-crossfade: 700ms;

  /* --- Hero entrance ------------------------------------------------------
     Two beats with no overlap: the shape settles completely, then the text
     comes in line by line. The text start is derived, not typed, so moving
     the shape duration or the pause cannot leave the two beats overlapping. */
  --hero-in-shape-delay: 200ms; /* lead-in before anything moves */
  --hero-in-shape-dur: 1100ms;
  --hero-in-shape-ease: cubic-bezier(0.16, 1, 0.3, 1);
  --hero-in-shape-from: 0.7;    /* scale it grows from */
  --hero-in-shape-off: -18%;    /* how far it sits toward the corner */
  --hero-in-pause: 50ms;        /* dead air between the beats — still a beat */
  --hero-in-line-dur: 600ms;
  /* Stagger, not duration. Each line still takes --hero-in-line-dur; they just
     start closer together, so every step begins while the one before it is
     roughly a fifth of the way through. */
  --hero-in-line-step: 130ms;
  /* The last two beats — header, then the bottom line — ride in almost on top
     of each other, so they read as one moment rather than two steps. */
  --hero-in-tail-step: 60ms;

  /* --- Mobile menu ---------------------------------------------------------
     The hero's motion at a menu's tempo. Same keyframes, same easing, only the
     numbers change: a menu is a response to a tap, so it has to land while the
     finger is still on the glass. The panel is ~38% of the hero shape's 1100ms
     and the whole thing is done in ~700ms against the hero's 2140. */
  --menu-in-panel-dur: 420ms;
  --menu-in-item-dur: 260ms;
  --menu-in-item-step: 55ms;
  /* Items start at 80% of the panel — "almost finished", not after. */
  --menu-in-items-at: calc(var(--menu-in-panel-dur) * 0.8);
  /* Closing is one collapse, not the opening played backwards at length. */
  --menu-out-dur: 180ms;
  --hero-in-lift: 20px;
  --hero-in-text-at: calc(var(--hero-in-shape-delay) + var(--hero-in-shape-dur)
    + var(--hero-in-pause));

  /* --- Buttons ---------------------------------------------------------- */
  /* Ghost at rest, filled on hover by a circular wipe out of the top-left
     corner: the hero shape's entrance borrowed wholesale, same easing token,
     shorter, because a button is a much smaller distance to cross. */
  --btn-fill-dur: 520ms;
  --btn-fill-ease: var(--hero-in-shape-ease);
  /* Leaving is one collapse, not the entrance replayed backwards — the same
     call the mobile menu makes with --menu-out-dur. */
  --btn-fill-out: 200ms;
  /* 100% of a circle() resolves against sqrt(w² + h²) / sqrt(2), so a circle
     centred on the top-left corner needs sqrt(2) ~ 141.5% to reach the far
     corner. 150% is that with slack for any padding the button ever takes. */
  --btn-fill-reach: 150%;
  /* The label flips as the fill passes under it, so it is paced against the
     wipe rather than against the wipe's stated duration: --btn-fill-ease is
     heavily front-loaded, and the fill has covered the whole label inside the
     first quarter of --btn-fill-dur. Measured, not guessed — see README. */
  --btn-ink-dur: 140ms;

  /* --- Selected projects gallery ---------------------------------------- */
  /* The space each project row keeps on either side of its hairline. One value
     for both, so the rule always sits midway between two rows rather than
     belonging to the one above it.

     The between-items step, so it is the scale's 40/56 pair rather than a
     clamp sweeping through every number in between. The projects gallery page
     uses the same two values for the gap between its cards — same
     relationship, same numbers. */
  /* Where a stacked Selected-projects row comes to rest: clear of the sticky
     header, on the same line as the rail label beside it. */
  --stack-top: calc(var(--header-h) + var(--sp-6));

  --gallery-row-pad: var(--sp-10);

  /* Arrow cursor for the project cards. Hotspot is the arrow's centre. */
  --cursor-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='34' height='34' viewBox='0 0 34 34'%3E%3Cpath d='M7 17h20M19 9l8 8-8 8' fill='none' stroke='%23FFFFFF' stroke-width='4.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M7 17h20M19 9l8 8-8 8' fill='none' stroke='%231b1a26' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") 17 17, pointer;

  /* --- Motion ----------------------------------------------------------- */
  --dur-fast: 160ms;
  --dur: 320ms;
  --dur-slow: 620ms;
  --ease: cubic-bezier(0.2, 0.6, 0.2, 1);

  /* --- The step (motion language, Oct 2026) ------------------------------
     Hover fills what you can click, in steps: surfaces and commands (cards,
     the nav, buttons, the logo) fill from the leading edge in two equal
     bands, the top one first, the other a beat behind. Words draw a line.
     One geometry and one easing at every scale — the hero shape's own steps,
     in miniature. Consumed by assets/css/motion.css.

     Set --step-stagger to 0 and --step-trail to 1 and the two bands move as
     one — that is the whole difference between one band and two. */
  --step-ease: cubic-bezier(0.16, 1, 0.3, 1);
  --step-in: 360ms;
  /* Leaving is one collapse, never the arrival played backwards. */
  --step-out: 180ms;
  /* The lag lives on the SMALL things. A step is readable by its size on
     screen and how long it stays there: on a card even a short offset is a
     100px stair, but on a 40px button a 40ms offset is a few pixels for a few
     frames — a glitch. So on buttons and the logo the second band starts
     --step-stagger late and travels --step-trail times slower. The first band
     is untouched, so the feedback itself is never late; only the echo is. */
  --step-stagger: 100ms;
  --step-trail: 1.6;
  /* The same step at card scale. A card is many times a button's area, so
     the bands take longer to cross it — but the stair is big enough to read
     without help, so no extra lag: a plain stagger, both bands at one speed. */
  --step-card-in: 480ms;
  --step-card-stagger: 60ms;
  --step-card-trail: 1;

  /* --- The About fill (Oct 2026) -----------------------------------------
     The About paragraph's words go from a dimmer ink to full ink as it moves
     up the window, scroll-driven (motion.css). --fill-from is where they
     start: the band's ink mixed this far into the violet.
     40% (2.72:1 on --ground-ink) is a deliberate exception to the 4.5 rule
     (cursor-system, Oct 2026): at 66%, the faintest that clears 4.5 (4.82),
     the fill was too subtle to see. The unfilled state is transient — every
     word reaches full ink by the time the paragraph is mid-window — and
     without scroll-driven animations or with reduced motion the paragraph is
     full ink throughout. 55% is 3.84, 45% 3.05, 35% 2.41, if it needs tuning.
     The contrast-audit sweep will flag the start colour; that is expected. */
  --fill-from: 40%;
  /* Where the fill runs in the paragraph's pass through the window ("cover"
     range): it starts --fill-start in, the last word lands --fill-span later,
     and one word takes --fill-word. */
  --fill-start: 18%;
  --fill-span: 30%;
  --fill-word: 8%;
  /* What a surface fills with on hover — every card and the nav, whatever
     the cover: the link blue at 12% in the plate. Interaction has one colour
     family, so the fill comes from it rather than from each cover. Ink on it
     14.2, muted 4.87 (the meta line still steps up to ink on hover). */
  --hover-fill: color-mix(in srgb, var(--link-ink) 12%, var(--color-plate));
  /* "You are here": the same blue one step deeper, so a hovered item can
     never be mistaken for the current one — hover previews the selected
     state, it does not duplicate it. Ink on it 12.2. */
  --selected-fill: color-mix(in srgb, var(--link-ink) 22%, var(--color-plate));
  /* The same pair where the header stands on the violet band: the light ink
     mixed into the band, hover lighter than "you are here". The text on them
     stays --ink-on-dark. */
  --hover-fill-on-dark: color-mix(in srgb, var(--ink-on-dark) 12%, var(--ground-ink));
  --selected-fill-on-dark: color-mix(in srgb, var(--ink-on-dark) 22%, var(--ground-ink));

  /* --- Hero entrance: the step at hero scale (Round 2, Oct 2026) ----------
     Row by row, top to bottom: each line's left word wipes in, its step of the
     shape fills from the left, then its right word wipes in. Same easing as
     every hover (--step-ease). The old entrance's --hero-in-* tokens above
     stay as they were: the frozen v0 home still runs on them. Consumed by
     home.css; the gate that holds the shape for its gradient is motion.js. */
  --hero-wipe-row: 90ms;     /* desktop: one row to the next (3 rows) */
  --hero-wipe-row-m: 70ms;   /* phone: 7 rows, so they run closer */
  --hero-wipe-band: 480ms;   /* one row of the shape filling */
  --hero-wipe-word: 300ms;   /* one word wiping in */
  --hero-wipe-lead: 40ms;    /* a row's band starts this long after its left word */
  --hero-wipe-lag: 110ms;    /* ...and its right word this long after the band */
  --hero-wipe-tail: 420ms;   /* the paragraph and the bottom row, last */
  /* The one hover colour for words on light grounds: the body ink, deepened.
     The same value the nav already uses, under a name that is not the nav's. */
  --hover-ink: var(--nav-hover-ink);
}

/* Below ~360px the 23vw step still reaches into the closing row's line, and the
   clamp floor cannot help — vw dominates that far down. Pull the step in so the
   row keeps its strip of ground beside it. */
@media (max-width: 22.5rem) {
  :root {
    --fc-w: 3rem;
  }
}

/* The page's arrow: the hero's scroll arrow and every footer's "Top" are one
   arrow, so they read one height. 22.4px from 48rem is 1.4x the 16px it
   shipped at; the width follows the SVG's 12:16 viewBox, and the stroke
   scales with it because it is drawn in viewBox units. Below 48rem both keep
   their own size (the hero's phone arrow is sized to the tag beside it). */
@media (min-width: 48rem) {
  :root {
    --hero-arrow-h: 1.4rem;
  }
}

/* The between-items step goes roomy on the desktop grid. 62rem is the site's
   one collapse point — the rail, the case-study row and the Selected projects
   row all change there, so the spacing changes with them rather than on a
   breakpoint of its own. */
@media (min-width: 62rem) {
  :root {
    --gallery-row-pad: var(--space-56);
  }
}
