/* ============================================
   wavy · studio (2027)
   Figma: wavy - Website, frames at 1920 × 1080.
   1rem = 16px at 1920 wide, so every Figma px value
   divides by 16 and the page scales as one artwork.
   ============================================ */

@font-face {
    font-family: 'Akagi Pro';
    src: local('Akagi Pro Medium'), local('AkagiPro-Medium');
    font-weight: 500;
    font-style: normal;
    font-display: swap;
}

:root {
    /* Figma styles */
    --wavy: #f7f3ef;          /* "wavy" */
    --ink: #191919;           /* "Brand Black" */
    --black: #191919;         /* Brand Black that never flips (the menu remaps --ink) */
    --ink-rgb: 25 25 25;      /* the same two inks, for alpha */
    --wavy-rgb: 247 243 239;
    --edge-rgb: 255 255 255;  /* a glass card's edges */
    --card: #fff;
    --glass: rgba(56, 56, 56, 0.6);
    --glass-solid: rgba(46, 46, 46, 0.85);
    --glass-shadow: rgba(0, 0, 0, 0.28);
    --zoom-ground: #212121;
    --talk-bg: #191919;
    --talk-fg: #fff;
    --talk-ring-rgb: 255 255 255;
    --ink-deep: #11141a;      /* Overlap ground */
    --tech-grey: #b2b2b2;     /* "tech grey" */
    --light-grey: #e6e6e7;    /* "Brand Light Grey" */
    --signal: #1ebeff;        /* Overlap grid blue */
    --lavender: #744bc4;      /* WebGL noise fill, col 1 */
    --lilac: #d3c1f7;         /* WebGL noise fill, col 2 */
    --shadow-rgb: 0, 0, 0;

    --font-display: 'Hanken Grotesk', 'Inter', system-ui, sans-serif;
    --font-ui: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
    --font-body: 'akagi-pro', 'Akagi Pro', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;

    /* the one curve: cubic-bezier(0.8, 0, 0, 1) */
    --ease-wavy: cubic-bezier(0.8, 0, 0, 1);
    --ease-out: cubic-bezier(0.16, 1, 0.3, 1);

    --gutter: 8.5rem;          /* 136px */
    --frame-top: 3.75rem;      /* 60px — nav top, and the menu's gap */
    --menu-gap: 3.75rem;
    /* between a title and the paragraphs under it, on every page: the
       thesis frame's own gap, 86px under its title at 1920 x 1080 where
       it was drawn. The frame is laid out by the window's height, so on
       a laptop, where the type stops shrinking at 12px, the thesis still
       keeps about 86px there: that's the floor */
    --title-gap: max(5.375rem, 86px);
}

@media (min-width: 900px) {
    html { font-size: clamp(12px, 0.8333vw, 20px); }
}

@media (max-width: 899px) {
    :root {
        --gutter: 1.25rem;
        --frame-top: 1rem;
        --menu-gap: 0.75rem;
        /* the same share of the title as the thesis keeps: nine tenths */
        --title-gap: 2.5rem;
    }
}

/* ============================================
   THEMES — the page is written light and the stage, the menu and the
   talk panel dark, so each theme flips one of the two: it swaps the
   tokens over that surface, and everything on it follows. The hero,
   the thesis and the footer sit on the gradient in both.
   ============================================ */
html[data-theme="dark"] { --after-from: #191919; --after-to: #191919; background: #191919; }
html[data-theme="dark"] body { background: #191919; }
html[data-theme="dark"] .section {
    --ink: #f7f3ef;
    --ink-rgb: 247 243 239;
    --wavy: #191919;
    --wavy-rgb: 25 25 25;
    --card: #232323;
    color: var(--ink);
}

html[data-theme="light"] {
    --after-from: #fff;
    --after-to: #f7f3ef;
    --talk-bg: #fff;
    --talk-fg: #18181b;
    --talk-ring-rgb: 25 25 25;
    background: #f7f3ef;
}
html[data-theme="light"] :is(.stage__bg, .work, .work-hud) {
    --black: #f7f3ef;
    --wavy: #191919;
    --wavy-rgb: 25 25 25;
    --edge-rgb: 25 25 25;
    --glass: rgba(255, 255, 255, 0.6);
    --glass-solid: rgba(255, 255, 255, 0.88);
    --glass-shadow: rgba(25, 25, 25, 0.08);
}
html[data-theme="light"] :is(.menu, .talk) {
    --black: #fff;
    --wavy: #191919;
    --wavy-rgb: 25 25 25;
    --zoom-ground: #f4f2ef;
    --ink-on-slab: #fff;
}
html[data-theme="light"] .nav-pill { --ink: #fff; --wavy: #191919; --wavy-rgb: 25 25 25; }
html[data-theme="light"] .nav-logo img { filter: brightness(0); }

/* the switch wipes across from the button that was pressed */
::view-transition-old(root),
::view-transition-new(root) { animation: none; mix-blend-mode: normal; }
::view-transition-new(root) { z-index: 1; }

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html.lenis, html.lenis body { height: auto; }

body {
    font-family: var(--font-body);
    background: var(--wavy);
    color: var(--ink);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    overflow-x: clip;
}

a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
img, svg, canvas { display: block; }
::selection { background: var(--lavender); color: var(--wavy); }

:focus-visible { outline: 2px solid var(--signal); outline-offset: 3px; border-radius: 4px; }
[tabindex="-1"]:focus { outline: none; }

/* ============================================
   GRAIN — Osmo Supply: Film Grain Effect
   ============================================ */
.grain {
  position: fixed;
  inset: 0%;
  z-index: 999;
  pointer-events: none;
  background: url("../img/v2/noise.avif") 50% 50% / 7.8125em repeat;
  opacity: 0.5;
  filter: contrast(125%) brightness(60%);
}

/* the grain rides over the menu too */
.grain { z-index: 2000; mix-blend-mode: overlay; opacity: 0.55; }

/* Optional: Animated Film Grain Effect */
.grain[data-grain-animate="true"] {
  animation: grain 0.6s steps(1, end) infinite;
}

@keyframes grain {
  0%   { background-position: 0em 0em; }
  20%  { background-position: -0.5em 0.3em;}
  40%  { background-position: 0.4em -0.2em;}
  60%  { background-position: -0.3em -0.4em;}
  80%  { background-position: 0.2em 0.5em;}
}

@media (prefers-reduced-motion: reduce) {
    .grain[data-grain-animate="true"] { animation: none; }
}
/* wavy: and it holds still while the theme wipes across (see theme.js),
   so the whole screen isn't repainted under every frame of the wipe */
html.is-wiping .grain[data-grain-animate="true"] { animation-play-state: paused; }

/* ============================================
   Page transition (Osmo Supply · Double Wipe)
   ============================================ */
.transition {
  z-index: 100;
  pointer-events: none;
  position: fixed;
  inset: 0;
  overflow: clip;
}

.transition__panels {
  width: 100%;
  height: 100%;
  display: flex;
  position: absolute;
  top: 0;
  left: 0;
}

.transition__panel-1 {
  background-color: #a6c1d1;
  width: 100%;
  height: 100%;
  position: absolute;
  top: 100%;
}

.transition__panel-2 {
  background-color: #091521;
  background-image: linear-gradient(90deg, #1e3b4d, #091521);
  width: 100%;
  height: 100%;
  position: absolute;
  top: 100%;
}

/* wavy: over every section but under the menu, the nav and the chat
   (1000 and up), so the pill holds still while the page changes under
   it and an open menu closes on top; and in our own grounds: white then
   the off-white ground in light, off-black then the brand black in
   dark. The second panel is the one that holds the screen and lifts off
   the next page, so it's that page's ground */
.transition { z-index: 900; }
.transition__panel-1 { background-color: #fff; }
.transition__panel-2 { background-color: #f7f3ef; background-image: none; }
html[data-theme="dark"] .transition__panel-1 { background-color: #232323; }
html[data-theme="dark"] .transition__panel-2 { background-color: #191919; }
/* arriving by one: the second panel is over the screen from the first
   paint, until the page is ready to be shown (see page-wipe.js) */
html.is-arriving .transition__panel-2 { transform: translateY(-100%); }

/* blending the whole screen on every frame costs a phone more than the
   grain gives it at that pixel density */
@media (max-width: 899px), (hover: none) {
    .grain { display: none; }
}

/* ============================================
   SHARED MOTION PRIMITIVES
   ============================================ */

/* word / letter masks — content starts 100% below and
   slides into view through a clip, never a fade */
.w {
    display: inline-block;
    vertical-align: top;
    clip-path: inset(-0.1em -0.08em -0.2em -0.08em);
}
.w > .w__i { display: inline-block; will-change: transform; }

/* line masks — the same move, a whole line at a time */
.ln { display: block; clip-path: inset(-0.1em -0.1em -0.25em -0.1em); }
.ln > .ln__i { display: block; will-change: transform; }

/* letter roll on hover: two stacked copies inside a one-line
   clip, each letter trails the one before it */
.roll { display: inline-flex; white-space: pre; }
.roll__c {
    display: inline-flex;
    flex-direction: column;
    height: 1.15em;
    line-height: 1.15;
    overflow: clip;
}
.roll__c > span {
    display: block;
    transition: transform .7s var(--ease-wavy);
    transition-delay: calc(var(--i, 0) * 16ms);
}
:is(a, button, .cap):hover .roll__c > span,
:is(a, button):focus-visible .roll__c > span,
.bento-cell--avail:hover .avail__big .roll__c > span { transform: translateY(-100%); }

/* ============================================
   NAV — the landing page's dark pill (Figma node 3291:1318), in
   the landing page's px: 350 × 48 at any screen, not the fluid rem
   ============================================ */
.header {
    position: fixed;
    top: var(--frame-top);
    left: var(--gutter);
    z-index: 1100;
    pointer-events: none;
}

.nav-pill {
    background-color: var(--ink);
    border-radius: 8px;
    padding: 12px 24px;
    width: 350px;
    pointer-events: auto;
    overflow: hidden;
    /* holds the edge against a ground of its own colour */
    box-shadow: inset 0 0 0 1px rgb(var(--wavy-rgb) / 0.1);
    transition: box-shadow .5s var(--ease-out);
}
/* open: the panel starts as exactly this rectangle in exactly this
   ink, so the pill drops its body and border and the panel grows on
   as the pill itself. Only the logo and the toggle stay, flying to
   their places in the panel's top band. */
.nav-pill.is-open {
    background-color: transparent;
    box-shadow: inset 0 0 0 1px rgb(var(--wavy-rgb) / 0);
    overflow: visible;
    pointer-events: none;
    transition: none;
}
.nav-pill.is-open :is(.nav-logo, .nav-toggle) { pointer-events: auto; }

.nav-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
}

.nav-logo img { width: 43px; height: 19px; }

/* light and dark: a disc, half of it filled */
.theme-toggle {
    display: grid;
    place-items: center;
    width: 28px;
    height: 24px;
    color: var(--wavy);
}
.theme-toggle i {
    display: block;
    width: 15px;
    height: 15px;
    border-radius: 50%;
    box-shadow: inset 0 0 0 1.5px currentColor;
    background: linear-gradient(90deg, currentColor 50%, transparent 50%);
    transition: rotate .7s var(--ease-wavy);
}
.theme-toggle:hover i { rotate: 180deg; }
html[data-theme="light"] .theme-toggle i { rotate: 180deg; }
html[data-theme="light"] .theme-toggle:hover i { rotate: 0deg; }

.nav-toggle {
    position: relative;
    width: 34px;
    height: 24px;
}

.toggle-line {
    position: absolute;
    left: 8px;
    display: block;
    width: 18px;
    height: 1.5px;
    background-color: var(--wavy);
    transition: transform 0.6s var(--ease-wavy);
}
.toggle-line:first-child { top: 8px; }
.toggle-line:last-child { top: 14.5px; }
/* drawn as paths once they can bend, to the same pixel at rest */
.toggle-line.has-wave { background: none; }
.toggle-wave { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.toggle-wave path { fill: none; stroke: var(--wavy); stroke-width: 1.5; }

/* X state for toggle */
.nav-pill.expanded .toggle-line:first-child { transform: translateY(3.25px) rotate(45deg); }
.nav-pill.expanded .toggle-line:last-child { transform: translateY(-3.25px) rotate(-45deg); }

@media (max-width: 700px) {
    .nav-pill { width: calc(100vw - 2 * var(--gutter)); }
}

/* Background overlay - 30% black */
.menu-overlay {
    position: fixed;
    inset: 0;
    background-color: rgba(0, 0, 0, 0.3);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.6s var(--ease-wavy), visibility 0s linear 0.6s;
    z-index: 1000;
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
}
.menu-overlay.active { opacity: 1; visibility: visible; transition-delay: 0s; }

/* ============================================
   MENU — the pill itself grows into a full-screen panel,
   60px off every edge, with one even frame of padding inside
   ============================================ */
.menu {
    position: fixed;
    inset: 0;
    z-index: 1050;
    /* Bencho tokens, mapped for the ink panel: ink flips to the
       cream on a dark surface */
    --ink: var(--wavy);
    --fill-on: var(--wavy);
    --fill-slab: var(--wavy);
    --font-ui: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
    --menu-pad: 2rem;      /* the frame inside the panel, the same on every side */
    --cell-pad: 1.5rem;    /* a cell's own padding: the logo lines up with its type */
    color: var(--wavy);
}
.menu[hidden] { display: none; }

/* the visible panel starts at --menu-gap (the clip); the frame
   of padding starts inside it. The top is the bar's to hold. */
.menu__inner {
    position: absolute;
    inset: 0;
    background: var(--black);
    padding: var(--menu-gap) calc(var(--menu-gap) + var(--menu-pad)) calc(var(--menu-gap) + var(--menu-pad));
    clip-path: inset(50% 50% 50% 50% round 0.5rem);
    display: flex;
    flex-direction: column;
}

/* the band above the grid: the frame above and below a 24px row,
   so the logo and the meta sit dead centre in it */
.menu__bar {
    flex: none;
    height: calc(var(--menu-pad) * 2 + 24px);
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 var(--cell-pad);
}
.menu__slot { display: block; flex: none; }
.menu__slot--logo { width: 43px; height: 19px; }
.menu__slot--toggle { width: 34px; height: 24px; }
.menu__bar-meta {
    display: flex;
    gap: 2rem;
    align-items: center;
    font: 500 0.8125rem/1 var(--font-ui);
    color: rgb(var(--wavy-rgb) / 0.6);
}
.menu__bar-meta time { color: var(--wavy); font-variant-numeric: tabular-nums; }
.menu__status { display: inline-flex; align-items: center; gap: 0.5rem; color: var(--wavy); }
.menu__esc {
    border: 1px solid rgb(var(--wavy-rgb) / 0.2);
    border-radius: 0.375rem;
    padding: 0.3rem 0.5rem;
    font-size: 0.6875rem;
}

/* white, in the ink of whatever it sits on, not a status green */
.pulse {
    width: 0.5rem; height: 0.5rem;
    border-radius: 50%;
    background: var(--wavy);
    box-shadow: 0 0 0 0 rgb(var(--wavy-rgb) / 0.6);
    animation: pulse 2s infinite;
    display: inline-block;
}
@keyframes pulse {
    0% { box-shadow: 0 0 0 0 rgb(var(--wavy-rgb) / 0.55); }
    70% { box-shadow: 0 0 0 0.6rem rgb(var(--wavy-rgb) / 0); }
    100% { box-shadow: 0 0 0 0 rgb(var(--wavy-rgb) / 0); }
}

/* the index keeps its quarter; the four widgets share the rest evenly */
.bento {
    position: relative;
    flex: 1;
    min-height: 0;
    display: grid;
    grid-template-columns: minmax(0, 3fr) minmax(0, 4.5fr) minmax(0, 4.5fr);
    grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
    gap: 0.75rem;
}

.bento-cell {
    position: relative;
    border-radius: 1.5rem;
    background: rgb(var(--wavy-rgb) / 0.035);
    box-shadow: inset 0 0 0 1px rgb(var(--wavy-rgb) / 0.07);
    overflow: hidden;
    transition: background-color .5s var(--ease-out), box-shadow .5s var(--ease-out);
}
.bento-cell:hover { background: rgb(var(--wavy-rgb) / 0.06); box-shadow: inset 0 0 0 1px rgb(var(--wavy-rgb) / 0.14); }

/* on open each cell is drawn first, like a spec: a hairline traces
   the box, the box fills in behind it, then its contents arrive */
.bento-cell.is-spec,
.bento-cell.is-spec:hover { background-color: rgb(var(--wavy-rgb) / 0); box-shadow: inset 0 0 0 1px rgb(var(--wavy-rgb) / 0); }
.bento-spec {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: 4;
    overflow: visible;
    pointer-events: none;
}
.bento-spec rect { fill: none; stroke: rgb(var(--wavy-rgb) / 0.45); stroke-width: 1; }
.menu-ln { display: block; }

.bento-cell--links     { grid-column: 1; grid-row: 1 / span 2; }
.bento-cell--globe     { grid-column: 2; grid-row: 1; }
.bento-cell--dial      { grid-column: 3; grid-row: 1; }
.bento-cell--particles { grid-column: 2; grid-row: 2; }
.bento-cell--avail     { grid-column: 3; grid-row: 2; }

.bento-cell__mask { height: 100%; overflow: clip; border-radius: inherit; }
.bento-cell__body {
    position: relative;
    height: 100%;
    display: flex;
    flex-direction: column;
    padding: var(--cell-pad);
}

.bento-cell__head { display: flex; flex-direction: column; gap: 0.625rem; position: relative; z-index: 2; pointer-events: none; }
.bento-label {
    font: 500 0.75rem/1 var(--font-ui);
    color: rgb(var(--wavy-rgb) / 0.5);
    letter-spacing: 0.01em;
}
.bento-title {
    font: 500 1.375rem/1.1 var(--font-ui);
    letter-spacing: -0.02em;
    color: var(--wavy);
}
.bento-title--lg { font-size: 2.5rem; letter-spacing: -0.035em; }
.bento-hint {
    position: relative;
    z-index: 2;
    font: 400 0.75rem/1 var(--font-ui);
    color: rgb(var(--wavy-rgb) / 0.4);
    pointer-events: none;
}
/* what the hint turns into once the form has been pressed all the way round */
.bento-door { color: var(--wavy); pointer-events: auto; }

.bento-stage {
    flex: 1;
    min-height: 0;
    position: relative;
    display: grid;
    place-items: center;
    margin: 0.5rem 0;
}
/* centred by position, not by the grid: a 480px canvas in a
   280px cell would otherwise overflow from its start edge */
.bento-stage__fit {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%) scale(var(--fit, 1));
    transform-origin: center;
}

/* the widget cells are wide and short now: words on the left,
   the thing itself standing the full height on the right */
.bento-cell--split .bento-cell__head,
.bento-cell--split .bento-hint { max-width: 42%; }
/* each title is two short lines, wider than that column on a small
   screen: they run on toward the globe and the dial rather than break */
.bento-cell--split .bento-cell__head { max-width: none; }
.bento-cell--split .bento-title { white-space: nowrap; }
.bento-cell--split .bento-hint { margin-top: auto; line-height: 1.35; }
.bento-cell--split .bento-stage {
    position: absolute;
    top: 0.75rem;
    right: 0.75rem;
    bottom: 0.75rem;
    left: 40%;
    margin: 0;
}

/* expand: the globe opens up over the whole grid. A rounded
   square, 48px with a 16px corner at full size */
.bento-expand {
    position: absolute;
    top: 1rem;
    right: 1rem;
    z-index: 3;
    width: max(3rem, 40px);
    height: max(3rem, 40px);
    border-radius: max(1rem, 12px);
    display: grid;
    place-items: center;
    color: var(--wavy);
    background: rgb(var(--wavy-rgb) / 0.06);
    box-shadow: inset 0 0 0 1px rgb(var(--wavy-rgb) / 0.16);
    transition: background-color .3s, color .3s, box-shadow .3s, transform .6s var(--ease-wavy);
}
.bento-expand svg {
    width: 40%;
    height: 40%;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.4;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.bento-expand:hover { background: var(--wavy); color: var(--black); box-shadow: none; transform: scale(1.08); }

.bento-zoom {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    height: 100%;
    z-index: 5;
    border-radius: 1.5rem;
    background: var(--zoom-ground);
    box-shadow: inset 0 0 0 1px rgb(var(--wavy-rgb) / 0.1);
}
.bento-zoom[hidden] { display: none; }
.bento-zoom__body {
    position: relative;
    height: 100%;
    display: grid;
    grid-template-columns: minmax(15rem, 24rem) minmax(0, 1fr);
    gap: 2rem;
    padding: var(--cell-pad);
}
.bento-zoom__side { display: flex; flex-direction: column; gap: 2rem; min-height: 0; }
.bento-zoom__stage { position: relative; min-width: 0; min-height: 0; }
.zoom-flip { position: absolute; inset: 0; transform-origin: 50% 50%; }
.zoom-readout { margin-top: auto; }
.zoom-readout__city { font: 500 3rem/1 var(--font-ui); letter-spacing: -0.04em; }
.zoom-readout__time { margin-top: 0.75rem; font: 500 0.9375rem/1.35 var(--font-ui); color: rgb(var(--wavy-rgb) / 0.6); }
.zoom-cities { display: flex; flex-direction: column; gap: 0.875rem; }
.zoom-cities__list { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.zoom-city {
    font: 500 0.75rem/1 var(--font-ui);
    padding: 0.55rem 0.8rem;
    border-radius: 99px;
    box-shadow: inset 0 0 0 1px rgb(var(--wavy-rgb) / 0.16);
    transition: background-color .3s, color .3s, box-shadow .3s;
}
.zoom-city:hover { box-shadow: inset 0 0 0 1px rgb(var(--wavy-rgb) / 0.55); }
.zoom-city[aria-pressed="true"] { background: var(--wavy); color: var(--black); box-shadow: none; }

/* links, centred in the cell whatever sits above and below them */
.bento-cell--links .bento-cell__body { display: grid; grid-template-rows: 1fr auto 1fr; }
.bento-cell--links .bento-label { grid-row: 1; align-self: start; justify-self: start; }
.bento-cell--links .menu-links { grid-row: 2; }
.bento-cell--links .menu-social { grid-row: 3; }
.menu-links { list-style: none; display: flex; flex-direction: column; gap: 0.25rem; }
.menu-links a {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    font: 500 clamp(1.5rem, min(2.9vw, 5vh), 3.5rem)/1.05 var(--font-ui);
    letter-spacing: -0.035em;
    padding: 0.2rem 0;
    transition: color .4s;
}
/* the capabilities' morph where the numbers were, at 8px: still and
   dim until its link is hovered, and centered on the words' capitals */
.menu-links__mark {
    flex: none;
    display: grid;
    font-size: 0.5rem;
    width: 1.25rem;
    color: rgb(var(--wavy-rgb) / 0.45);
    transition: color .4s;
}
.menu-links a:hover .menu-links__mark { color: var(--wavy); }
.menu-links__mark .ldr { justify-items: start; color: inherit; }
.menu-links__mark .ldr-slot { width: 1em; height: 1em; }
.menu-links__mark .ldr-slot > * { scale: 1; }
.menu-links__mark :is(.ldr-morph, .ldr-morph i) { width: 1em; height: 1em; }
/* the morph's corners are set in pixels for 24px; this small they'd
   read as a circle throughout, so here they go by the share instead */
.menu-links__mark .ldr-morph i { border-radius: 25%; animation-name: ldr-shape-small; }
@keyframes ldr-shape-small {
  0%, 12% { border-radius: 25%; rotate: 0deg; scale: 1; }
  50%, 62% { border-radius: 50%; rotate: 90deg; scale: 0.9; }
  100% { border-radius: 25%; rotate: 180deg; scale: 1; }}
.menu-links:hover a { color: rgb(var(--wavy-rgb) / 0.35); }
.menu-links:hover a:hover { color: var(--wavy); }
.menu-social {
    align-self: end;
    margin-top: 2rem;
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}
.menu-social a {
    font: 500 0.75rem/1 var(--font-ui);
    padding: 0.55rem 0.8rem;
    border-radius: 99px;
    box-shadow: inset 0 0 0 1px rgb(var(--wavy-rgb) / 0.16);
    transition: background-color .3s, color .3s;
}
.menu-social a:hover { background: var(--wavy); color: var(--black); }

/* availability */
/* the label up top like the other cells, the date and the seats in
   the middle of what's left, and the pill at the foot */
.bento-cell--avail .bento-cell__body { display: grid; grid-template-rows: auto 1fr auto; }
.avail__now { align-self: center; display: flex; flex-direction: column; gap: 1rem; }
.bento-cell--avail:hover { background: rgb(var(--wavy-rgb) / 0.035); box-shadow: inset 0 0 0 1px rgb(var(--wavy-rgb) / 0.07); }
.avail__big {
    position: relative;
    font: 700 clamp(3.5rem, min(6vw, 10vh), 7rem)/0.9 var(--font-display);
    letter-spacing: -0.06em;
}
/* the ’27: the fourth letter on */
.avail__big .roll__c:nth-child(n + 4) { color: rgb(var(--wavy-rgb) / 0.4); }
.avail__seats { position: relative; display: flex; align-items: center; gap: 0.6rem; font: 500 0.9375rem/1.2 var(--font-ui); }
.bento-cell--avail .btn--talk {
    justify-self: start;
    box-shadow: inset 0 0 0 1px rgb(var(--wavy-rgb) / 0.14);
}

/* the ground behind it on hover: the home page's gradient, muted and
   at the thesis's lava-lamp pace, eased in rather than wiped, under the
   hero's light tint */
.avail__scene {
    position: absolute;
    inset: 0;
    border-radius: inherit;
    overflow: hidden;
    opacity: 0;
    transition: opacity 1.2s var(--ease-out);
    pointer-events: none;
}
.bento-cell--avail.is-lit .avail__scene { opacity: 1; }
/* the words over it are light in both themes, as on the hero */
.bento-cell--avail.is-lit { --wavy: #f7f3ef; --wavy-rgb: 247 243 239; color: var(--wavy); }
/* drawn half as big again as the cell, so the cell holds the colour in
   the middle of the gradient and none of its pale rim, which the light
   words would wash out on */
.avail__scene :is(.avail__still, canvas) { position: absolute; left: -25%; top: -25%; width: 150%; height: 150%; object-fit: cover; }

/* the notification — sized in em off its stage width so the
   557 × 143 card is the same share of a 754-wide frame anywhere */
.ui-notes {
    position: relative;
    display: block;
    font-size: calc(100cqi / 47.125);
    width: 34.8125em;
    height: 8.9375em;
}
.ui-note {
    position: absolute;
    inset: 0;
    background: #fff;
    border-radius: 3.75em;
    box-shadow: 0 3.375em 10.25em rgba(0, 0, 0, 0.12);
    display: flex;
    align-items: center;
    padding: 0 2.125em;
    gap: 2.25em;
}
/* with no script to take turns, the first one stays */
.ui-notes > .ui-note:not(:first-child) { visibility: hidden; }
.ui-note__icon {
    position: relative;
    flex: none;
    width: 5em; height: 4.9635em;
    border-radius: 1.5em;
    overflow: hidden;
    display: grid;
    place-items: center;
}
.ui-note__icon img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; filter: blur(0.4em) saturate(1.1); transform: scale(1.2); }
.ui-note__icon span { position: relative; font: 500 0.723em/1 var(--font-body); color: #fff; }
.ui-note__text {
    flex: 1;
    font: 300 1.985em/1.2 var(--font-body);
    color: var(--tech-grey);
    letter-spacing: -0.01em;
}
.ui-note__text b { font-weight: 300; color: var(--black); }
/* the ring is the button's own edge, Figma's 1 in 80. The arrow stays
   centered in it, hover or not */
.ui-note__go {
    flex: none;
    width: 5em; height: 5em;
    border-radius: 50%;
    background: #fff;
    box-shadow: inset 0 0 0 max(1px, 0.0625em) var(--light-grey);
}
.ui-note__go svg { display: block; width: 100%; height: 100%; fill: var(--black); }

/* ══ Blueprint loader (lab/BlueprintLoader.tsx) ═════════════════
   The real card sits in the flow (it is what the plan is measured
   from) and is masked away until its wipe; the plan is an SVG over
   it, overflowing so the guides and the size label can run past the
   card. data-beat drives it: rest → plan → reveal → done. Every
   duration is scaled by --bpl-k, the Speed knob.
   wavy: Bencho's tokens are set here, on the loader's own root, from
   the note's own: its slab is the white card, and the plan is drawn
   in white over the gradient. The root sits over the stage the note
   did, the note fills it, and leave is a fifth beat, the card fading
   out where it is. The dashed lines trace along themselves (see
   .bpl-trace) on --bpl-draw, the menu boxes' kind of curve */
.bpl {
  position: absolute;
  inset: 0;
  --fill-on: var(--black);
  --fill-slab: var(--card);
  --pane-edge: rgb(var(--edge-rgb) / 0.2);
  font-family: var(--font-ui);
  color: var(--fill-on, var(--ink));
  cursor: pointer;
  --bpl-dot: rgb(255 255 255 / 0.6);
  --bpl-line: #fff;
  --bpl-guide: rgb(255 255 255 / 0.85);
  --bpl-bar: rgb(255 255 255 / 0.5);
  --bpl-ink: #fff;
  --bpl-mute: rgb(255 255 255 / 0.75);
  --bpl-wash: rgb(255 255 255 / 0.12);
  --bpl-out: cubic-bezier(0.22, 1, 0.36, 1);
  --bpl-draw: cubic-bezier(0.65, 0, 0.35, 1);}

/* over the card, not under it: the card's slab would hide the plan
   the moment it arrived; on top, the rules dissolve over the text */
.bpl-plan { position: absolute; left: 0; top: 0; z-index: 1; overflow: visible; pointer-events: none; }

.bpl-dot { fill: var(--bpl-dot); }

/* wavy: the grid itself is always there; its diagonals come and go */
.bpl-dot, .bpl-frame, .bpl-rule, .bpl-box { opacity: 0; }

.bpl-frame rect { fill: none; stroke: var(--bpl-line); stroke-width: 1; stroke-dasharray: 2 4; }

.bpl-guide { stroke: var(--bpl-guide); stroke-width: 1; stroke-dasharray: 2 4; }

.bpl-label { fill: var(--bpl-ink); /* size: the <text>'s own, in the drawing */ font-family: var(--font-ui); font-weight: 500; font-variant-numeric: tabular-nums; letter-spacing: 0em; }

.bpl-rule > rect:first-child { fill: var(--bpl-bar); }

.bpl-caret { fill: var(--bpl-ink); opacity: 0; }

.bpl-box > rect:first-child { fill: none; stroke: var(--bpl-line); stroke-width: 1; stroke-dasharray: 3 3; }

/* the plan draws */
/* wavy: the dots cascade in a diagonal at a time from the top left */
.bpl[data-beat="plan"] .bpl-dot {
  opacity: 1;
  transition: opacity calc(200ms * var(--bpl-k)) ease calc(var(--gd) * var(--bpl-k));}

/* wavy: the outline, the guides and the boxes are there at once, and
   draw in through their traces; the size comes up once they're down */
.bpl-trace { fill: none; stroke: #fff; stroke-width: 6; }
.bpl[data-beat="plan"] :is(.bpl-frame, .bpl-box) { opacity: 1; }
.bpl[data-beat="plan"] .bpl-trace {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset calc(var(--t) * var(--bpl-k)) var(--bpl-draw) calc(var(--d) * var(--bpl-k));}
.bpl[data-beat="reveal"] .bpl-trace { stroke-dashoffset: 0; }
.bpl-label { opacity: 0; }
.bpl[data-beat="plan"] .bpl-label {
  opacity: 1;
  transition: opacity calc(200ms * var(--bpl-k)) ease calc(560ms * var(--bpl-k));}
.bpl[data-beat="reveal"] .bpl-label { opacity: 1; }

.bpl-rule { clip-path: inset(-4px 100% -4px -4px); }

.bpl[data-beat="plan"] .bpl-rule {
  opacity: 1;
  clip-path: inset(-4px -4px -4px -4px);
  transition: clip-path calc(360ms * var(--bpl-k)) var(--bpl-draw), opacity 120ms;
  transition-delay: calc(var(--d) * var(--bpl-k));}

.bpl[data-beat="plan"] .bpl-caret {
  opacity: 1;
  transition: opacity calc(120ms * var(--bpl-k)) ease;
  transition-delay: calc((var(--d) + 300ms) * var(--bpl-k));}

/* the real card wipes down over it; the plan goes as it does */
.bpl[data-beat="reveal"] .bpl-caret { opacity: 1; }

.bpl[data-beat="reveal"] .bpl-rule { clip-path: inset(-4px -4px -4px -4px); }

/* the plan does not get wiped: it fades and softens away under the
   card, so for a moment the two are seen together (wavy: its lines
   do, quicker, 400 after 120, from 600 after 160; the dots cascade
   back out the way they came in) */
.bpl[data-beat="reveal"] :is(.bpl-frame, .bpl-rule, .bpl-box) {
  opacity: 0;
  filter: blur(2px);
  transition: opacity calc(400ms * var(--bpl-k)) ease calc(120ms * var(--bpl-k)), filter calc(400ms * var(--bpl-k)) ease calc(120ms * var(--bpl-k));}
.bpl[data-beat="reveal"] .bpl-dot {
  opacity: 0;
  transition: opacity calc(200ms * var(--bpl-k)) ease calc(var(--gd) * 0.8 * var(--bpl-k));}

/* wavy: and the note's words blur in one by one as it comes, the
   site's text reveal (trv-blur, 900 on an expo-out, 55 apart) */
.bpl-word { display: inline-block; }
.bpl:is([data-beat="reveal"], [data-beat="done"]) .bpl-word {
  animation: trv-blur calc(900ms * var(--bpl-k)) cubic-bezier(0.16, 1, 0.3, 1) calc((120ms + var(--wi) * 55ms) * var(--bpl-k)) both;}

/* wavy: only the wipe. The profile card's padding, corner and slab
   are the note's own here, and its shadow goes on the root (below),
   since the mask would cut a shadow off at the card's edge */
.bpl-card {
  /* a soft wipe: the edge spreads over most of the card's height,
     and the card fades up as it comes */
  opacity: 0;
  -webkit-mask: linear-gradient(to bottom, #000 20%, transparent 80%) 0 100% / 100% 300% no-repeat;
  mask: linear-gradient(to bottom, #000 20%, transparent 80%) 0 100% / 100% 300% no-repeat;}

.bpl[data-beat="reveal"] .bpl-card, .bpl[data-beat="done"] .bpl-card {
  opacity: 1;
  -webkit-mask-position: 0 0;
  mask-position: 0 0;
  transition: mask-position calc(450ms * var(--bpl-k)) var(--bpl-out), -webkit-mask-position calc(450ms * var(--bpl-k)) var(--bpl-out), opacity calc(300ms * var(--bpl-k)) ease;}

[data-stroke="on"] .bpl-card { box-shadow: 0 0 0 1px var(--pane-edge); }

/* wavy: the note's shadow, on a shape of its own under the card,
   coming up with the card and going with it */
.bpl > .ui-note { box-shadow: none; }
.bpl::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 3.75em;
  box-shadow: 0 3.375em 10.25em rgba(0, 0, 0, 0.12);
  opacity: 0;}

.bpl:is([data-beat="reveal"], [data-beat="done"])::before {
  opacity: 1;
  transition: opacity calc(450ms * var(--bpl-k)) ease;}

/* wavy: leave. The card fades where it stands, the wipe held open, and
   the plan stays away */
.bpl[data-beat="leave"] .bpl-card {
  opacity: 0;
  -webkit-mask-position: 0 0;
  mask-position: 0 0;
  transition: opacity calc(500ms * var(--bpl-k)) ease;}

.bpl[data-beat="leave"]::before {
  opacity: 0;
  transition: opacity calc(500ms * var(--bpl-k)) ease;}

@media (prefers-reduced-motion: reduce) {
  .bpl * { transition-duration: 160ms !important; animation: none !important; }
}

@media (max-width: 899px) {
    .menu { --menu-pad: 0.75rem; --cell-pad: 1.25rem; }
    .menu__bar-meta { gap: 1rem; }
    .menu__esc, .menu__clock { display: none; }
    .bento {
        display: flex;
        flex-direction: column;
        overflow-y: auto;
        overscroll-behavior: contain;
        border-radius: 1.5rem;
    }
    .bento-cell { flex: none; }
    .bento-cell--links { min-height: 70svh; }
    .bento-cell--globe, .bento-cell--dial, .bento-cell--particles { height: 24rem; }
    .bento-cell--avail { min-height: 18rem; }
    .bento-cell--split .bento-cell__head,
    .bento-cell--split .bento-hint { max-width: none; }
    .bento-cell--split .bento-stage { position: relative; inset: auto; flex: 1; margin: 0.5rem 0; }
    .bento-zoom__body { grid-template-columns: 1fr; grid-template-rows: auto minmax(0, 1fr); gap: 1rem; }
    .bento-zoom__side { gap: 1rem; padding-right: 3rem; }
    .bento-title--lg { font-size: 1.75rem; }
    .zoom-readout { margin-top: 0; }
    .zoom-readout__city { font-size: 2rem; }
    .zoom-cities__list { max-height: 5.5rem; overflow-y: auto; }
    .menu-links a { font-size: 2.5rem; }
}

/* ============================================
   SEQUENCE STAGE
   ============================================ */
.sequence { position: relative; }

.stage {
    position: relative;
    height: 100vh;
    height: 100lvh;
    overflow: hidden;
    color: var(--wavy);
}
/* the tornado's ground; once the wash has filled the frame it is the
   ground the wash settles on, which --after-mix eases into the
   sections' own as the stage leaves */
main { --after: color-mix(in srgb, var(--after-to) calc(var(--after-mix, 0) * 100%), var(--after-from)); }
.stage__bg { position: absolute; inset: 0; background: var(--black); }
.stage__bg.is-white { background: var(--after); }

/* ── Work: Osmo Supply 3D Cards Tornado ─────────────── */
.work { position: absolute; inset: 0; }

.cards-tornado {
  /* scroll drives this one, so it scrolls the page instead of
     catching the touch, and there is nothing to grab */
  cursor: auto;
  touch-action: pan-y;
  width: 100%;
  height: 100%;
  position: relative;
  overflow: clip;
}

.cards-tornado__collection {
  width: 100%;
  height: 100%;
  position: relative;
}

.cards-tornado__list {
  /* 15em card width: 356px at 1920 (356 / 15 = 23.733px em) */
  font-size: clamp(17px, 1.236vw, 23.733px);
  width: 100%;
  height: 100%;
  /* flat, so the cards stack in order of depth rather than being sorted
     in 3D: a sorted card's glass can't see the cards behind it */
  perspective: 75em;
  position: relative;
}

.cards-tornado__item {
  backface-visibility: visible;
  will-change: transform;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  visibility: hidden;
}

/* the landing page's glass card. One fixed box, so every tile's media
   sits in the same rectangle and the gradient can land on it exactly */
.tornado-glass-card {
  position: relative;
  pointer-events: none;
  -webkit-user-select: none;
  user-select: none;
  width: 15em;
  height: 19em;
  padding: 1.1em;
  display: flex;
  flex-direction: column;
  gap: 1em;
  border-radius: 1.75em;
}

/* the glass on a layer of its own, so a card can be drawn: --fill,
   --pic and --type are set from the tornado, one card at a time */
.tornado-glass-card::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1px solid rgb(var(--edge-rgb) / 0.22);
  background: var(--glass);
  -webkit-backdrop-filter: blur(1.4em) saturate(1.3);
  backdrop-filter: blur(1.4em) saturate(1.3);
  box-shadow:
    inset 0 1px 0 rgb(var(--edge-rgb) / 0.18),
    0 12px 40px var(--glass-shadow);
  opacity: var(--fill, 1);
}

/* round the back of the orbit: the same grey, solid enough not to need
   the blur, swapped while the card is edge-on so it never shows */
.cards-tornado__item.is-turned .tornado-glass-card::before {
  background: var(--glass-solid);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/* a phone gets the same grey without the live blur or the shadow:
   together they were most of what made the tiles heavy to scroll */
@media (max-width: 767px) {
  .tornado-glass-card::before {
    background: var(--glass-solid);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    box-shadow: inset 0 1px 0 rgb(var(--edge-rgb) / 0.18);
  }
}

/* dims a card toward the ground, set from the tornado */
.tornado-glass-card__shade {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--black);
  opacity: 0;
  pointer-events: none;
}

/* the hairline that traces the box, the menu's */
.tornado-glass-card__spec {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}
.tornado-glass-card__spec rect { fill: none; stroke: rgb(var(--wavy-rgb) / 0.55); stroke-width: 1; }

.tornado-glass-card__media {
  flex: 1 1 auto;
  min-height: 0;
  border-radius: 0.65em;
  background: rgb(var(--edge-rgb) / 0.04);
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  clip-path: inset(calc((1 - var(--pic, 1)) * 100%) 0 0 0 round 0.65em);
}

:is(.tornado-glass-card__media-video, .tornado-glass-card__media-img) {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
  scale: calc(1.25 - 0.25 * var(--pic, 1));
}

.tornado-glass-card__media { background-position: center; background-size: cover; }
.tornado-glass-card__media::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(135deg, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0) 55%);
  box-shadow: inset 0 0 0 1px rgb(var(--edge-rgb) / 0.12);
  opacity: var(--fill, 1);
  pointer-events: none;
}

.tornado-glass-card__lockup {
  flex: none;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.85em;
  height: 3.4em;
  position: relative;
  opacity: var(--type, 1);
  translate: 0 calc((1 - var(--type, 1)) * 1em);
}

.tornado-glass-card__title {
  font: 600 clamp(14px, 1.042vw, 20px)/1 var(--font-ui);
  letter-spacing: -0.03em;
  color: rgb(var(--wavy-rgb) / 0.98);
  flex-shrink: 0;
  max-width: 55%;
}

.tornado-glass-card__meta {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  text-align: right;
  gap: 0.4em;
  flex: 1;
  min-width: 0;
  padding-top: 0.08em;
}

.tornado-glass-card__tagline {
  font: 400 clamp(12px, 0.833vw, 16px)/1.35 var(--font-ui);
  letter-spacing: -0.01em;
  color: rgb(var(--wavy-rgb) / 0.92);
}

.tornado-glass-card__category {
  font: 400 clamp(11px, 0.729vw, 14px)/1.35 var(--font-ui);
  color: rgb(var(--wavy-rgb) / 0.42);
}

@media (max-width: 767px) {
  .cards-tornado__list { font-size: clamp(12px, 3.7vw, 18px); }
  /* a phone's tile is small, so its words are sized off the tile. The
     name takes two lines, which leaves the words beside it the room */
  .tornado-glass-card { height: 21.9em; }
  .tornado-glass-card__lockup { height: 5.8em; gap: 0.7em; }
  .tornado-glass-card__title { max-width: 4.6em; font-size: 1.02em; line-height: 1.1; }
  .tornado-glass-card__tagline { font-size: 0.82em; line-height: 1.3; }
  .tornado-glass-card__category { font-size: 0.74em; line-height: 1.3; }
}

/* HUD */
.work-hud {
    position: absolute;
    left: var(--gutter);
    right: var(--gutter);
    bottom: 2.75rem;
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: end;
    font: 500 0.8125rem/1.2 var(--font-ui);
    color: var(--wavy);
    opacity: 0;
    pointer-events: none;
    z-index: 3;
}
/* tiles pass right under it on a phone, so it sits on the ground */
@media (max-width: 767px) {
    .work-hud::before {
        content: "";
        position: absolute;
        z-index: -1;
        left: calc(-1 * var(--gutter));
        right: calc(-1 * var(--gutter));
        bottom: -2.75rem;
        height: calc(100% + 6rem);
        background: linear-gradient(transparent, var(--black) 42%);
        pointer-events: none;
    }
}
.work-hud__left, .work-hud__right { display: flex; flex-direction: column; gap: 0.4rem; }
.work-hud__right { align-items: flex-end; }
.work-hud__label, .work-hud__kind { opacity: 0.55; }
.work-hud__count { font-variant-numeric: tabular-nums; }
.work-hud__name {
    font: 700 clamp(2.5rem, 4.2vw, 5rem)/1 var(--font-display);
    letter-spacing: -0.05em;
    clip-path: inset(-0.1em -0.2em -0.2em -0.2em);
    text-align: center;
    min-width: 20ch;
}
.work-hud__name-inner { display: inline-block; }
.work-hud__scroll::after { content: " ↓"; }
@media (max-width: 899px) {
    .work-hud { grid-template-columns: 1fr 1fr; row-gap: 0.75rem; bottom: 1.5rem; }
    .work-hud__name { grid-column: 1 / -1; grid-row: 1; text-align: left; min-width: 0; font-size: 2.25rem; white-space: nowrap; }
}

/* ── Hero (Figma 3290:1175) ─────────────────────────── */
.hero { position: absolute; inset: 0; z-index: 2; }

.hero__media {
    position: absolute;
    inset: 0;
    clip-path: inset(0px 0px 0px 0px round 0px);
    overflow: hidden;
}
/* square, and as big as the longer side of the screen: the
   gradient is square, so cover-fit is an exact fit, and scaling
   this box down to a card's long side gives the card's own crop */
.hero__media-inner {
    position: absolute;
    left: 50%;
    top: 50%;
    width: max(100vw, 100lvh);
    height: max(100vw, 100lvh);
    translate: -50% -50%;
    transform-origin: 50% 50%;
}
.hero__fallback,
.hero__canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.hero__canvas,
.flow-canvas { opacity: 0; transition: opacity 1.2s var(--ease-out); }
:is(.hero__canvas, .flow-canvas).is-ready { opacity: 1; }
.hero__tint { position: absolute; inset: 0; background: rgba(25, 25, 25, 0.12); }

.hero__content { position: absolute; inset: 0; pointer-events: none; }
.hero__content :is(a, button) { pointer-events: auto; }

/* the type both full-bleed frames share: the hero and the thesis */
/* the headline is the thesis title's size and ghosts in a line at a
   time (see the .trv override after the text reveal block) */
:is(.hero__headline, .case__headline) {
    position: absolute;
    left: var(--gutter);
    top: calc(196 / 1080 * 100%);
    width: 13em;
    max-width: calc(100vw - 2 * var(--gutter));
    font: 500 6rem/1 var(--font-ui);
    letter-spacing: -0.045em;
    color: var(--wavy);
    text-wrap: balance;
}
.hero__headline:not(.is-revealed) { visibility: hidden; }
.case__headline:not(.is-revealed) { opacity: 0; transition: opacity .35s var(--ease-out); }
/* the three small blocks share one row. The tracks are the Figma
   frame's columns, start to start, as shares of the width between
   the gutters (695 / 834 / 119 of 1648 at 1920), so the frame is
   exact wherever the type scales with the screen; below 1440 the
   type stops shrinking and the blocks narrow instead of colliding */
:is(.hero__cols, .case__cols) {
    position: absolute;
    top: calc(474 / 1080 * 100%);
    left: var(--gutter);
    right: var(--gutter);
    display: grid;
    grid-template-columns: minmax(0, 695fr) minmax(0, 834fr) minmax(4.25rem, 119fr);
    align-items: start;
}
:is(.hero__col, .case__col) {
    font: 500 1.21rem/1.33 var(--font-body);
    color: var(--wavy);
}
.hero__col--1, .case__col--1 { width: min(33.5rem, 100% - 2rem); }
.hero__col--2, .case__col--2 { width: min(24.8125rem, 100% - 2rem); }
.hero__col--3 { width: 4.25rem; }
/* the wordmark is sized by the width and the row placed by the height,
   so on a wide, short screen they meet. There the row rises to clear
   the wordmark (its letters top out 24.22vw above the foot) by 1.5rem,
   and the headline rises as far, though never up under the nav pill.
   5.175rem is the subtitle's two lines, 12rem the headline's two.
   Shorter than the frame can hold, it's laid out at that height and
   the wordmark runs off the foot instead of up into the words. Two
   things set that height: the row 1.5rem past the headline's two
   lines while both keep the frame's spacing (278 / 1080 of it is
   13.5rem at 52.45rem), and the headline pinned under the pill with
   the row and the wordmark's 24.22vw below it */
@media (min-width: 900px) {
    .hero__content {
        min-height: max(52.45rem, var(--frame-top) + 48px + 21.675rem + 24.22vw);
        --lift: max(0px, 24.22vw + 5.175rem + 1.5rem - 606 / 1080 * 100%);
        --head-top: max(196 / 1080 * 100% - var(--lift), min(196 / 1080 * 100%, var(--frame-top) + 48px + 1.5rem));
    }
    .hero__headline { top: var(--head-top); }
    /* with no room for both, the row stops a gap short of the headline */
    .hero__cols { top: max(474 / 1080 * 100% - var(--lift), var(--head-top) + 12rem + 1.5rem); }
}

/* centred on the nav pill's row */
.hero__actions {
    position: absolute;
    top: calc(var(--frame-top) + 24px);
    right: var(--gutter);
    translate: 0 -50%;
    display: flex;
    gap: clamp(12px, 1rem, 16px);
}

/* the landing page's buttons (style.css .btn): 14px type, 16 / 32
   padding, fully round. Held at that size on smaller screens too,
   where the fluid rem would shrink them */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: clamp(14px, 1rem, 18px) clamp(28px, 2rem, 36px);
    border-radius: 9999px;
    font: 500 clamp(14px, 0.875rem, 16px)/1.2 var(--font-body);
    border: 1px solid transparent;
    white-space: nowrap;
    transition: background-color .5s var(--ease-out), color .5s var(--ease-out), border-color .5s var(--ease-out);
}
.btn__mask { display: inline-flex; overflow: clip; }
/* the secondary: nothing but an edge, in the colour Let's talk is filled with */
.btn--ghost { background: none; border-color: var(--talk-bg); color: var(--talk-bg); }
/* on the hero it sits on the gradient, which stays the same in both themes */
.hero__actions .btn--ghost { border-color: #fff; color: #fff; }
/* the hero's way to the work: just its words, black on the gradient in
   either theme, still rolling on hover. The edge and padding stay,
   unseen, so it keeps the button's room beside Let's talk */
.btn--text { background: none; border-color: transparent; color: #191919; }

/* the way in to the talk panel: a pill the height of its neighbour,
   with the hero's orb for an avatar, glowing, in a ring of its own */
.btn--talk {
    gap: 12px;
    min-height: calc(2 * clamp(14px, 1rem, 18px) + 1.2em + 2px);
    padding: 0 clamp(22px, 1.75rem, 32px) 0 10px;
    background: var(--talk-bg);
    color: var(--talk-fg);
}
.btn--talk.is-talking { visibility: hidden; }
/* arriving (cta-grow.js): the fill grows out from behind the orb. It
   starts as a circle that grows with the ball, hidden behind it (--dot),
   then opens to the whole pill (--grow). The orb's centre is the border
   and padding in from the left, and halfway down. The fill is a real
   round box whose edges move, not a clip: iPhone Safari drew a clip's
   round ends square on some frames, so the pill flickered as it grew */
.btn--talk.is-growing {
    position: relative;
    isolation: isolate;
    background-color: transparent;
    transition: none;
    --c: 25px;
    --d: calc(14px * var(--dot, 1));
}
.btn--talk.is-growing::before {
    content: '';
    position: absolute;
    z-index: -1;
    top: calc((50% - var(--d)) * (1 - var(--grow, 1)) - 1px);
    right: calc((100% - var(--c) - var(--d)) * (1 - var(--grow, 1)) - 1px);
    bottom: calc((50% - var(--d)) * (1 - var(--grow, 1)) - 1px);
    left: calc((var(--c) - var(--d)) * (1 - var(--grow, 1)) - 1px);
    border-radius: 9999px;
    background: var(--talk-bg);
}
.talk-orb {
    position: relative;
    flex: none;
    width: 28px;
    height: 28px;
    transition: scale .7s var(--ease-wavy);
}
.btn--talk:hover .talk-orb { scale: 1.08; }
.talk-orb > * { position: absolute; border-radius: 50%; }
/* the extra ring: a hair of edge round a little frosted air */
.talk-orb__ring {
    inset: -4px;
    background: rgb(var(--talk-ring-rgb) / 0.04);
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
    box-shadow: inset 0 0 0 1px rgb(var(--talk-ring-rgb) / 0.1);
}
.talk-orb__glow {
    inset: -3px;
    background: conic-gradient(from 40deg, #a98bf0, #f58fc4, #ffd06e, #8fb2f5, #a98bf0);
    filter: blur(5px);
    opacity: 0.75;
    animation: orb-glow 9s linear infinite;
}
@keyframes orb-glow { to { rotate: 360deg; } }
.talk-orb__ball { inset: 0; overflow: hidden; }
/* well in on the swirl, closer than the hero ever gets. The ball itself
   holds still: everything happens inside it, a storm that turns without
   stopping, while the drift, on a clock of its own, keeps it from ever
   coming round the same way */
.talk-orb__ball :is(img, canvas) {
    position: absolute;
    inset: -85%;
    width: 270%;
    height: 270%;
    object-fit: cover;
    animation: orb-glow 13s linear infinite, orb-drift 7.3s ease-in-out infinite alternate;
}
@keyframes orb-drift {
    from { translate: 9% -7%; }
    to   { translate: -8% 9%; }
}
@media (prefers-reduced-motion: reduce) {
    .talk-orb__glow, .talk-orb__ball :is(img, canvas) { animation: none; }
}

.hero__wordmark,
.footer__wordmark {
    position: absolute;
    left: 0;
    bottom: -0.1277em;
    font: 700 45.92vw/1 var(--font-display);
    letter-spacing: -0.06em;
    color: var(--wavy);
    white-space: nowrap;
    pointer-events: none;
}
.hero__letter {
    display: inline-block;
    /* the mask: open above and to the sides so the glyph's own
       overhang survives the -6% tracking, shut at the bottom */
    clip-path: inset(-40% -12% 0 -12%);
}
.hero__letter > span { display: inline-block; will-change: transform; }

@media (max-width: 899px) {
    :is(.hero__headline, .case__headline) { top: 14%; width: calc(100% - 2 * var(--gutter)); font-size: 2.5rem; }
    /* stacked, a paragraph's space apart */
    :is(.hero__cols, .case__cols) { top: 38.5%; display: flex; flex-direction: column; gap: 1em; font-size: 0.9375rem; }
    :is(.hero__col, .case__col) { font-size: inherit; width: auto; }
    .hero__col--3 { display: none; }
    .hero__actions { top: auto; bottom: 38vw; left: var(--gutter); right: auto; translate: none; gap: 0.5rem; }
}

/* ── The thesis ─────────────────────────────────────── */
.case {
    position: absolute;
    inset: 0;
    visibility: hidden;
    color: var(--wavy);
    z-index: 3;
}
.case__frame { position: absolute; inset: 0; clip-path: inset(0px 0px 0px 0px round 0px); overflow: hidden; }
/* the hero's square: scaled to a tile's long side it gives the tile's
   own crop, so the wash leaves the card without a jump */
.case__square {
    position: absolute;
    left: 50%;
    top: 50%;
    width: max(100vw, 100lvh);
    height: max(100vw, 100lvh);
    translate: -50% -50%;
    transform-origin: 50% 50%;
}
.case__square :is(.case__photo, canvas) {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.case__content { position: absolute; inset: 0; pointer-events: none; }
.case__title {
    position: absolute;
    top: calc(196 / 1080 * 100%);
    left: var(--gutter);
    right: var(--gutter);
}
/* the hero's first line wears the thesis line's type */
:is(.hero__col--1, .case__col--1) { font: 500 2.25rem/1.15 var(--font-ui); letter-spacing: -0.025em; width: min(30rem, 100% - 2rem); }
/* side by side, the columns line up along the tops of their capitals,
   not of their boxes: the big line has more room above its capitals, so
   lined up by box it sat low beside the small ones. Each is trimmed to
   its cap height, so every first line starts on one top line */
@media (min-width: 900px) {
    :is(.hero__col, .case__col) { text-box: trim-start cap alphabetic; }
}
.case__col--2 { width: min(30rem, 100% - 2rem); }
/* the same floor as the hero's: on a screen shorter than --least the
   thesis is laid out at that height, and the marquee runs off the foot
   instead of up into the words. The title's two lines and a 1.5rem gap
   fit the frame's 278 / 1080 between title and row from 52.45rem. The
   row's four lines, a gap, and the marquee's 9.9rem at its 40px +
   1.5rem fit the 606 / 1080 under the row from the second figure,
   which the first is above at every size the type takes */
@media (min-width: 900px) {
    .case { --least: max(52.45rem, (40px + 23.25rem) * 1080 / 606); }
    .case__content { min-height: var(--least); }
}
/* a phone has no room for the frame's fixed rows: the words stack from
   under the nav, clear of the marquee at the foot */
@media (max-width: 899px) {
    .case__content {
        display: flex;
        flex-direction: column;
        gap: 1.25rem;
        padding: calc(var(--frame-top) + 48px + 2.5rem) calc(var(--gutter) + 16px) 0;
    }
    .case__title, .case__cols { position: static; }
    .case__cols { gap: 0.875rem; }
    :is(.hero__col--1, .case__col--1) { font-size: 1.375rem; line-height: 1.2; }
    .case__col--2 { font-size: 0.9375rem; line-height: 1.4; }
}

/* ============================================
   SECTIONS
   ============================================ */
main { position: relative; }

.section {
    position: relative;
    padding: 12rem var(--gutter);
    background: var(--wavy);
    z-index: 1;
}

/* every section's title, its name anchored to the right of it */
.section__title {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 2rem;
}
.section__tag {
    flex: none;
    padding-top: 0.5rem;
    font: 500 1.21rem/1.33 var(--font-body);
    color: var(--ink);
}
.section__tag--light { color: var(--wavy); }
/* the paragraphs a title introduces start the thesis's distance under it */
.section__title + :is(.pair, .body, .partnership__intro) { margin-top: var(--title-gap); }
@media (max-width: 899px) {
    .section__title { flex-direction: column-reverse; gap: 1rem; }
    .section__tag { align-self: flex-start; padding-top: 0; font-size: 0.9375rem; }
}

.display {
    font: 500 6rem/1 var(--font-ui);
    letter-spacing: -0.045em;
    color: var(--ink);
    max-width: 16ch;
    text-wrap: balance;
}
.display--light { color: var(--wavy); }

.body { font: 500 1.21rem/1.5 var(--font-body); color: rgb(var(--ink-rgb) / 0.72); max-width: 36ch; }
.link { color: var(--ink); background: linear-gradient(currentColor, currentColor) 0 100% / 100% 1px no-repeat; transition: background-size .6s var(--ease-wavy); }
.link:hover { background-size: 0 1px; background-position: 100% 100%; }

/* a lead and what supports it, set the way the thesis sets them: the
   lead in the thesis line's type, the support in body type beside it,
   starting where the thesis's second column does. The standard for two
   paragraphs under a title, here and on the pages beyond */
.pair {
    display: grid;
    grid-template-columns: minmax(0, 695fr) minmax(0, 953fr);
    align-items: start;
}
.pair__lead { font: 500 2.25rem/1.15 var(--font-ui); letter-spacing: -0.025em; color: var(--ink); width: min(30rem, 100% - 2rem); }
.pair__body { font: 500 1.21rem/1.33 var(--font-body); color: rgb(var(--ink-rgb) / 0.72); width: min(30rem, 100% - 2rem); }
.pair__side { display: flex; flex-direction: column; gap: 3rem; }

@media (max-width: 899px) {
    .section { padding: 7rem var(--gutter); }
    .display { font-size: 2.75rem; }
    .body { font-size: 1.0625rem; }
    .pair { grid-template-columns: 1fr; gap: 0.875rem; }
    .pair__lead { font-size: 1.375rem; line-height: 1.2; width: auto; }
    .pair__body { font-size: 1.0625rem; line-height: 1.4; width: auto; }
    .pair__side { gap: 2rem; }
}

/* ══ Text reveal (Bencho) ═══════════════════════════════ */
.trv {
  /* as wide as its longest line, up to 320: left-aligned, and the
     box it sits centred in is the text's own */
  position: relative;
  width: max-content;
  max-width: 300px;
  margin: 0;
  font-size: 16px;
  line-height: 1.5;
  letter-spacing: -0.01em;
  color: var(--fill-on, var(--ink));
  text-wrap: pretty;
  cursor: pointer;
  user-select: none;
  outline: none;}

.trv:focus-visible { outline: 2px solid var(--signal, #2231dd); outline-offset: 6px; border-radius: 6px; }

/* a word never breaks across lines, even split into letters */
.trv-word { display: inline-block; white-space: nowrap; }
/* wavy: the quieter words in a title, so the ones that matter carry it */
.dim { opacity: 0.4; }

.trv-bit {
  display: inline-block;
  animation: trv-blur var(--trv-dur, 900ms) cubic-bezier(0.16, 1, 0.3, 1) var(--d, 0ms) both;}

.trv[data-look="rise"] .trv-bit { animation-name: trv-rise; }

.trv[data-look="fade"] .trv-bit { animation-name: trv-fade; }

@media (prefers-reduced-motion: reduce) {
  .trv-bit { animation: none; }
}

@keyframes trv-blur { from { opacity: 0; filter: blur(8px); translate: 0 0.3em; } }

@keyframes trv-rise { from { opacity: 0; translate: 0 0.6em; } }

@keyframes trv-fade { from { opacity: 0; } }

/* wavy: the same reveal at headline size. The demo box above is
   300px of 16px type; a headline wants its own measure. */
.trv.trv--display {
    width: auto;
    max-width: 16ch;
    font-size: 6rem;
    line-height: 1;
    letter-spacing: -0.045em;
    text-wrap: balance;
}
.trv--display.display--light { color: var(--wavy); }
[data-text-reveal]:not(.is-revealed) { visibility: hidden; }
/* in, and done with: nothing left that a move in the page could replay */
.trv.is-landed .trv-bit { animation: none; }
@media (max-width: 899px) { .trv.trv--display { font-size: 2.75rem; } }

/* wavy: the full-bleed frames' headlines run the same reveal, a line
   at a time, and keep the frame's own placement */
.trv:is(.hero__headline, .case__headline) {
    position: absolute;
    width: 13em;
    max-width: calc(100vw - 2 * var(--gutter));
    font-size: 6rem;
    line-height: 1;
    letter-spacing: -0.045em;
    color: var(--wavy);
    text-wrap: balance;
    cursor: default;
}
.trv.case__headline { position: relative; left: 0; top: 0; }
@media (max-width: 899px) {
    .trv:is(.hero__headline, .case__headline) { width: calc(100% - 2 * var(--gutter)); font-size: 2.5rem; }
    .trv.case__headline { width: 100%; }
}

/* ── Marquee, across the foot of the wash ── */
.marquee {
    position: absolute;
    left: 0;
    right: 0;
    bottom: calc(40px + 1.5rem);
    overflow: hidden;
    color: var(--wavy);
    pointer-events: none;
}
.marquee__track {
    display: flex;
    align-items: center;
    gap: 3rem;
    width: max-content;
    font: 600 9rem/1.1 var(--font-display);
    letter-spacing: -0.06em;
    white-space: nowrap;
    will-change: transform;
}
/* wavy: the craft forms between the words. A form only fills the
   middle of its canvas, so the margin brings the words back to the
   gap, and the spare room is where a burst of dots flies */
.marquee__track i {
    width: 10.4rem;
    aspect-ratio: 6 / 5;
    margin: 0 -2rem;
    flex: none;
    pointer-events: auto;
    cursor: pointer;
}
.marquee__track .ptc { width: 100%; height: 100%; }
.marquee__track .ptc-canvas { width: 100%; height: 100%; color: inherit; cursor: inherit; touch-action: pan-y; }
/* on a screen shorter than the thesis's --least, as far below the foot
   as the screen falls short */
@media (min-width: 900px) {
    .marquee { bottom: calc(40px + 1.5rem + min(0px, 100% - var(--least))); }
}
@media (max-width: 899px) {
    .marquee { bottom: calc(16px + 1rem); }
    .marquee__track { font-size: 4.5rem; gap: 1.5rem; }
    .marquee__track i { width: 5.2rem; margin: 0 -1rem; }
}

/* ── Partnership ── */
.partnership { background: var(--after); }
.partnership__intro { display: flex; justify-content: flex-end; }
.models {
    margin-top: 6rem;
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 1rem;
}
.model {
    position: relative;
    min-height: 46rem;
    padding: 2.25rem;
    border-radius: 2rem;
    background: var(--card);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    isolation: isolate;
}
/* the dots, behind the words */
.model__form {
    position: absolute;
    inset: 0;
    z-index: -1;
    width: 100%;
    height: 100%;
    color: var(--ink);
    pointer-events: none;
}
.model__tag { font: 500 0.9375rem/1 var(--font-ui); color: rgb(var(--ink-rgb) / 0.6); }
.model__title {
    margin-top: auto;
    font: 700 3rem/1 var(--font-display);
    letter-spacing: -0.045em;
}
/* 80% of the padded card is about 70% of the card */
.model__copy {
    margin-top: 1rem;
    font: 500 1.0625rem/1.45 var(--font-body);
    color: rgb(var(--ink-rgb) / 0.7);
    max-width: 80%;
    text-wrap: pretty;
}
@media (max-width: 899px) {
    .partnership__intro { display: block; }
    .models { grid-template-columns: 1fr; gap: 1rem; margin-top: 3rem; }
    .model { min-height: 32rem; }
    .model__title { font-size: 2.25rem; }
    .model__copy { max-width: none; }
}

/* ── Capabilities ── */
.caps { list-style: none; margin-top: 5rem; border-bottom: 1px solid rgb(var(--ink-rgb) / 0.12); }
.cap {
    display: grid;
    grid-template-columns: 6rem 1fr 26rem;
    align-items: center;
    gap: 2rem;
    padding: 1.75rem 0;
    border-top: 1px solid rgb(var(--ink-rgb) / 0.12);
    cursor: default;
    transition: color .5s var(--ease-out), padding .8s var(--ease-wavy);
}
/* each row is a link to its own page */
a.cap { cursor: pointer; }
.cap__mark { display: block; opacity: 0.45; transition: opacity .5s var(--ease-out); }
.cap:hover .cap__mark { opacity: 1; }
.cap__name { font: 500 4.25rem/1 var(--font-ui); letter-spacing: -0.045em; }
.cap__desc { font: 500 1.0625rem/1.4 var(--font-body); color: rgb(var(--ink-rgb) / 0.6); }
.caps:hover .cap { color: rgb(var(--ink-rgb) / 0.3); }
.caps:hover .cap:hover { color: var(--ink); padding-left: 1.5rem; }
.cap-preview {
    position: fixed;
    top: 0; left: 0;
    width: 22rem;
    aspect-ratio: 4 / 3;
    margin: -8.25rem 0 0 -11rem;
    pointer-events: none;
    z-index: 50;
    border-radius: 1.5rem;
    overflow: hidden;
    scale: 0;
    box-shadow: 0 2rem 6rem rgba(0, 0, 0, 0.18);
}
.cap-preview__inner { position: absolute; inset: 0; }
.cap-preview__inner video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
@media (max-width: 899px) {
    .cap { grid-template-columns: 2.5rem 1fr; gap: 0.75rem; }
    .cap__name { font-size: 2.25rem; }
    .cap__desc { grid-column: 2; }
    .cap-preview { display: none; }
}

/* ── System ── */
.system {
    display: grid;
    grid-template-columns: 1fr 47.125rem;
    gap: 5rem 6rem;
    align-items: center;
}
.system > .section__title { grid-column: 1 / -1; }
.system__copy { display: flex; flex-direction: column; gap: 2.5rem; }
.system__list { list-style: none; display: flex; flex-direction: column; border-top: 1px solid rgb(var(--ink-rgb) / 0.12); }
.system__list li {
    display: grid;
    grid-template-columns: 1.5rem 11rem 1fr;
    gap: 1rem;
    align-items: center;
    padding: 1.1rem 0;
    border-bottom: 1px solid rgb(var(--ink-rgb) / 0.12);
    font: 500 1rem/1.3 var(--font-body);
    color: rgb(var(--ink-rgb) / 0.6);
}
.system__list code { font: 500 0.9375rem/1 ui-monospace, 'SF Mono', Menlo, monospace; color: var(--ink); }
/* the row's mark, the morph's move once: a dim square that turns a
   quarter as it rounds into a full circle, landing with the morph's
   small give past its mark. 12px, like the capability marks */
.tick {
    width: 0.75rem; height: 0.75rem;
    border-radius: 25%;
    background: rgb(var(--ink-rgb) / 0.2);
    transition:
        border-radius .7s cubic-bezier(0.6, -0.12, 0.3, 1.22),
        rotate .7s cubic-bezier(0.6, -0.12, 0.3, 1.22),
        background-color .5s var(--ease-out);
}
.system__list li.is-done .tick { border-radius: 50%; rotate: 90deg; background: var(--ink); }

.system__visual {
    container-type: inline-size;
    position: relative;
    aspect-ratio: 754 / 964;
    border-radius: 3.75rem;
    overflow: hidden;
    display: grid;
    place-items: center;
}
.system__bg { position: absolute; inset: 0; width: 100%; height: 115%; top: -7.5%; object-fit: cover; }
@media (max-width: 899px) {
    .system { grid-template-columns: 1fr; gap: 2rem; }
    .system__list li { grid-template-columns: 1.5rem 1fr; }
    .system__list li span:last-child { grid-column: 2; }
    .system__visual { border-radius: 2rem; }
}

/* ── Close ── */
.close {
    position: relative;
    z-index: 1;
    min-height: 100vh;
    background: var(--ink);
    color: var(--wavy);
    overflow: hidden;
    display: flex;
    flex-direction: column;
}
.close__media { position: absolute; inset: 0; }
.close__media :is(img, canvas) { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.close__media img { opacity: 0.9; }
.close__media .flow-canvas.is-ready { opacity: 0.9; }
.close__media::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(25,25,25,0.15), rgba(25,25,25,0.55)); }
.close__inner {
    position: relative;
    padding: 12rem var(--gutter) 6rem;
    display: flex;
    flex-direction: column;
    gap: 2.5rem;
}
.close__actions { display: flex; gap: 0.75rem; flex-wrap: wrap; }
/* the address, written out beside the pill for anyone who'd rather
   just write, and for anything reading the page for a way in. White,
   like the hero's ghost button, since it sits on the gradient in both
   themes */
.close__actions .close__email {
    align-self: center;
    margin-left: 1rem;
    color: #fff;
    font: 500 clamp(14px, 0.875rem, 16px)/1.2 var(--font-body);
}
.footer {
    position: relative;
    margin-top: auto;
    height: 42vw;
}
.footer__row {
    position: relative;
    z-index: 2;
    display: flex;
    justify-content: space-between;
    gap: 2rem;
    padding: 0 var(--gutter);
    font: 500 0.875rem/1.4 var(--font-body);
    color: rgb(var(--wavy-rgb) / 0.8);
}
.footer__row time { font-variant-numeric: tabular-nums; }
.footer__wordmark { bottom: -0.1277em; }
/* the footer's wordmark at 40%, and on a screen with a pointer, frosted
   glass: the ground behind it blurred through the letters' own shapes
   (a mask clipped to the text). At home each letter carries its own
   frost, and its mask is an overflow clip there, since a clip-path would
   cut the frost off from what's behind. Phones get no live blur */
:is(.footer__wordmark:not([data-footer-wordmark]), [data-footer-wordmark] .hero__letter > span) { color: rgb(var(--wavy-rgb) / 0.4); }
@media (min-width: 900px) and (hover: hover) {
    :is(.footer__wordmark:not([data-footer-wordmark]), [data-footer-wordmark] .hero__letter > span) {
        color: transparent;
        background: rgb(var(--wavy-rgb) / 0.4);
        -webkit-background-clip: text;
        background-clip: text;
        -webkit-backdrop-filter: blur(24px);
        backdrop-filter: blur(24px);
        -webkit-mask-image: linear-gradient(#000 0 0);
        -webkit-mask-clip: text;
    }
    [data-footer-wordmark] .hero__letter { clip-path: none; overflow-x: visible; overflow-y: clip; }
    /* set tighter than their own width, the glyphs run past their boxes
       on the right, and the fill and the frost stop at the box: the box
       reaches out to cover them, and the margin takes the room back */
    [data-footer-wordmark] .hero__letter > span { padding-right: 0.12em; margin-right: -0.12em; }
    .footer__wordmark:not([data-footer-wordmark]) { padding-right: 0.12em; }
}
@media (max-width: 899px) {
    .close__inner { padding-top: 7rem; }
    .footer__row { flex-direction: column; gap: 0.5rem; }
    /* the row sits clear above the wordmark */
    .footer { height: calc(42vw + 7rem); }
}

/* ══ Dot globe (lab/Globe.tsx) ═══════════════════════════════════
   The canvas draws in this element's own colour — the page's ink,
   so it is dark dots on light and light dots on dark. */
.glb {
  display: block;
  color: var(--ink);
  cursor: grab;
  touch-action: none;}

.glb:active { cursor: grabbing; }
/* the open menu scrolls on a phone, so an up and down swipe that starts
   on one of its pieces has to carry on through it */
@media (max-width: 899px) {
    .menu :is(.glb, .ptc-canvas, .g-arc) { touch-action: pan-y; }
}

/* over a city: it has a name to show, so it reads as something to point at */
.glb[data-hover="true"] { cursor: pointer; }

/* ══ Particles ═════════════════════════════════════════════
   lab/Particles.tsx. The dots are drawn on a canvas in the
   element's own `color`, so the ink is set here and the theme
   carries it. */
.ptc {
  position: relative;
  display: grid;
  place-items: center;}

.ptc-canvas {
  display: block;
  color: var(--fill-on, var(--ink));
  cursor: crosshair;
  touch-action: none;}

/* ── sleep arc ─────────────────────────────────────────── */
/* ── sleep dial ─────────────────────────────────────────────
   No pane. The wheel and its pill, stacked, standing directly
   on whatever the section is made of. */
.slp {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  font-family: var(--font-ui);
  color: var(--fill-on, var(--ink));}

.slp-dial { position: relative; width: 288px; height: 288px; }

.g-arc {
  position: absolute;
  inset: 0;
  width: 288px; height: 288px;
  cursor: grab;
  touch-action: none;
  outline: none;}

/* ── a tick draws itself as it is taken into the range ──────
   The wave. Each newly-lit tick grows out from the inner ring
   rather than appearing at full length, and each waits a
   little longer than the one before it — so the range fills
   along its own length instead of switching on all at once.

   `--d` is how many ticks in from the nearer handle it is, set
   per line in GlassSet.tsx. Measuring from the NEARER end is
   what makes the ripple start where your finger is without the
   component being told which handle moved: the ticks beside it
   are the ones with the small numbers.

   `both`, so the tick is at dashoffset 1 — invisible — for the
   whole of its delay rather than sitting at full length until
   its turn comes. Without it the wave is a set of ticks
   blinking off and back on. */
.g-arc line[data-on] {
  animation: dial-fill 260ms cubic-bezier(0.22, 1, 0.36, 1) both;
  animation-delay: calc(var(--d, 0) * 16ms);}

@media (prefers-reduced-motion: reduce) {
  .g-arc line[data-on] { animation-duration: 1ms; animation-delay: 0ms; }
}

.g-arc[data-dragging="true"] { cursor: grabbing; }

/* ── the handles ────────────────────────────────────────────
   A grid with both in one cell, moved off it by transform.
   That is the shape liquid-gooey can measure: in flow, with a
   real box. The group is centred on the dial and the offsets
   are computed from the same angle the ticks use. */
.slp-knobs {
  position: absolute;
  left: 50%; top: 50%;
  margin: -144px 0 0 -144px;
  width: 288px; height: 288px;
  display: grid;
  place-items: center;
  pointer-events: none;}

/* undo the library's double-applied scale — see Gooey.tsx */
.slp-knobs > svg {
  transform: scale(calc(1 / var(--k, 1)));
  transform-origin: 0 0;}

/* wavy: the goo layer is drawn by our own SVG in layout px, so
   --k stays 1 and the rule above is a no-op; it sits over the
   whole dial rather than in the handles' grid cell */
.slp-goo {
  position: absolute;
  inset: 0;
  width: 288px; height: 288px;
  overflow: visible;
  filter: drop-shadow(0 2px 8px rgba(var(--shadow-rgb), 0.24));}

/* no background: <Liquid> paints the disc behind it, and a
   fill here would sit as a hard circle on the liquid edge */
/* 20, up from 17. It is the thing you grab and it was the
   smallest object on a 288px dial — and it just lost the ring
   that was standing in for a couple of pixels of size. */
.slp-knob {
  grid-area: 1 / 1;
  display: grid;
  place-items: center;
  width: 20px; height: 20px;
  border-radius: 999px;}

/* the fill's ink, not the theme's — same reason as the knob's
   ring: a dark fill in a light theme left this dark on dark */
.slp-pip {
  /* wavy: the handle is a cream slab on the ink menu, so its
     own ink is the dark one */
  --fill-on: var(--ink-on-slab, #191919);
  width: 5px; height: 5px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--fill-on, #17181a) 52%, transparent);}

.slp-mid {
  position: absolute;
  left: 0; right: 0;
  top: 50%;
  translate: 0 -50%;
  text-align: center;
  pointer-events: none;}

/* INTER, NOT DM MONO. Same reasoning as Balance: the only
   thing the monospaced face was buying is a stable digit
   advance so the figure does not jitter as it counts, and
   tabular-nums gives that in the proportional face. */
/* -0.045, the same tracking the wheel's readout took. At 34px
   the figures carry more than body copy wants, and this one
   has units set between them — the tighter the numerals sit,
   the more "7h 30m" reads as one measurement rather than as
   four things in a row. */
.slp-figure {
  font-family: var(--font-ui);
  font-size: 30px;
  font-weight: 500;
  letter-spacing: -0.03em;
  line-height: 1;
  font-variant-numeric: tabular-nums;}

/* ── ONE SIZE, TWO WEIGHTS OF ATTENTION ─────────────────────
   The units were 14px against a 34px figure — a two-and-a-half
   to one jump that made them read as footnotes stuck to the
   numbers rather than as part of the same reading. 24 is close
   enough that the line scans as one thing and still clearly
   subordinate, and the GREY is what keeps them subordinate:
   size was doing a job colour was already doing. */
.slp-unit {
  font-family: var(--font-ui);
  font-size: 20px;
  font-weight: 500;
  letter-spacing: -0.02em;
  /* 0.35 was tuned for type small enough to need the help; at
     this size it reads as almost absent */
  opacity: 0.42;
  margin-left: 3px;}

.slp-unit[data-pair="true"] { margin-right: 6px; }

@keyframes dial-fill {
  from { stroke-dashoffset: 1; }
  to { stroke-dashoffset: 0; }}

/* ============================================
   TALK — the onboarding: the pill grows into a panel the way the nav
   does, smaller, in the menu's ground. The dots on the left, the
   conversation on the right.
   ============================================ */
.talk-overlay { z-index: 1200; }
.talk {
    position: fixed;
    inset: 0;
    z-index: 1250;
    --ink: var(--wavy);
    --fill-on: var(--wavy);
    --talk-gap: var(--menu-gap);
    color: var(--wavy);
}
.talk[hidden] { display: none; }
.talk__inner {
    position: absolute;
    inset: 0;
    background: var(--black);
    clip-path: inset(50% 50% 50% 50% round 0.5rem);
}
.talk__panel {
    position: absolute;
    left: 50%;
    top: 50%;
    translate: -50% -50%;
    width: min(1120px, 100vw - 2 * var(--talk-gap));
    height: min(720px, var(--talk-vh, 100dvh) - 2 * var(--talk-gap));
    padding: 12px;
    border-radius: 28px;
    display: grid;
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    gap: 12px;
}
.talk__stage {
    position: relative;
    min-height: 0;
    border-radius: 20px;
    background: rgb(var(--wavy-rgb) / 0.035);
    box-shadow: inset 0 0 0 1px rgb(var(--wavy-rgb) / 0.07);
    overflow: hidden;
    transition: background-color .5s var(--ease-out), box-shadow .5s var(--ease-out);
}
.talk__stage.is-spec { background-color: transparent; box-shadow: inset 0 0 0 1px transparent; }
.talk__shape { position: absolute; inset: 24px 24px 56px; }
.talk__fit {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%) scale(var(--fit, 1));
    transform-origin: center;
}
.talk__status {
    position: absolute;
    left: 24px;
    bottom: 22px;
    display: flex;
    align-items: center;
    gap: 8px;
    font: 500 13px/1 var(--font-ui);
    color: rgb(var(--wavy-rgb) / 0.5);
}
.talk__chat {
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    padding: 8px 20px 12px 28px;
}
.talk__bar {
    flex: none;
    display: flex;
    align-items: center;
    gap: 16px;
    height: 44px;
    font: 500 13px/1 var(--font-ui);
    color: rgb(var(--wavy-rgb) / 0.5);
}
.talk__count { font-variant-numeric: tabular-nums; }
.talk__back { font: inherit; color: inherit; transition: color .3s; }
.talk__back:hover { color: var(--wavy); }
.talk__back[hidden] { display: none; }
.talk__close {
    position: relative;
    margin-left: auto;
    width: 34px;
    height: 34px;
    color: var(--wavy);
}
.talk__close::before,
.talk__close::after {
    content: "";
    position: absolute;
    left: 8px;
    top: 16px;
    width: 18px;
    height: 1.5px;
    background: currentColor;
    rotate: 45deg;
    transition: rotate .6s var(--ease-wavy);
}
.talk__close::after { rotate: -45deg; }
.talk__close:hover::before { rotate: 135deg; }
.talk__close:hover::after { rotate: 45deg; }

.talk__thread {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 16px 8px 20px 0;
    scrollbar-width: none;
}
.talk__thread::-webkit-scrollbar { display: none; }
/* the first turn takes the slack, so the talk sits at the foot */
.talk__thread > :first-child { margin-top: auto; }
/* flex: none, or a full thread squeezes a new turn to nothing while it
   opens, and it pops in at the end */
.talk-turn { flex: none; display: flex; flex-direction: column; }
.talk-turn + .talk-turn { margin-top: 22px; }
.talk-turn--done { gap: 14px; }
.talk-say {
    font: 500 clamp(26px, 2.6vw, 38px)/1.14 var(--font-ui);
    letter-spacing: -0.03em;
    color: var(--wavy);
}
/* the mask for each word. It has to be a real clip: with a clip-path the
   words still count as overflow while they rise, the thread grows to fit
   them and the whole conversation jumps up and sinks back */
.talk-say .w { overflow: clip; padding: 0.1em 0.08em 0.22em; margin: -0.1em -0.08em -0.22em; }
.talk-say em { font-style: normal; color: rgb(var(--wavy-rgb) / 0.5); }
.is-past > .talk-say { font-size: 15px; line-height: 1.35; letter-spacing: -0.01em; color: rgb(var(--wavy-rgb) / 0.5); }
.talk-you {
    align-self: flex-end;
    max-width: 85%;
    margin-top: 14px;
    padding: 10px 15px;
    border-radius: 18px 18px 4px 18px;
    background: rgb(var(--wavy-rgb) / 0.09);
    font: 500 15px/1.35 var(--font-ui);
    color: var(--wavy);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.talk__reply { flex: none; display: flex; flex-direction: column; gap: 10px; }
.talk__reply[hidden] { display: none; }
/* away, but still holding its space, so nothing above it moves */
.talk__reply.is-away { visibility: hidden; }
/* still there for the caret, so the keyboard stays up between two typed answers */
.talk__reply.is-away.is-keeping { visibility: visible; pointer-events: none; }
/* ══ Command bar (Bencho) ═════════════════════════════════════
   The block is React on liquid-gooey, and this site has no build,
   so it is ported by hand: the same SVG-filter silhouette layer
   under a crisp content layer, which is how the library draws it.
   Tokens: --fill-on is already --wavy here, --on-ink is --black,
   --ink-4 and --ink-5 are --wavy at .6 and .38, and --fill-slab is
   set below. The block's glass rim and test board have no surface
   to sit on here, so they stay behind.

   ── the split ────────────────────────────────────────────
   The bar and the send button are two bodies in one liquid
   group, so while the button is leaving the two are still one
   body of liquid, necking thinner until the goo gives up.

   ── THE BAR DOES NOT MOVE ───────────────────────────────
   Everything inside the bar travels with it, so the mic fading
   out would also slide sideways, and a thing that is leaving in
   two different ways at once reads as neither. So the whole
   offset goes on the button. The bar is fixed and the button
   walks out from under its right edge. The cost is that the
   composition is no longer centred at rest — it grows rightwards
   into the space the button will occupy. That is a fair trade
   for a field whose contents hold still. */
.talk__write {
    /* OPAQUE FILL. rgba glass won't do: the goo's alpha contrast
       erases anything under about 0.39 — a translucent bar would
       not look faint here, it would not be drawn at all. Mixed
       from the panel's own ink and ground so it still inverts
       with the theme. */
    --fill-slab: color-mix(in srgb, var(--wavy) 8%, var(--black));
    --cmd-btn: 46px;
    --cmd-gap: 8px;
    /* how far the button has to travel to be fused into the bar:
       its whole width plus the gap it would otherwise sit across */
    --cmd-tuck: calc(var(--cmd-btn) + var(--cmd-gap));
    /* the reference's curve — a little past its target and back,
       so the neck snaps rather than easing apart */
    --cmd-morph: 520ms cubic-bezier(0.22, 1.3, 0.71, 1);
    --cmd-trade: 420ms cubic-bezier(0.24, 1.34, 0.38, 1);
    position: relative;
}
.talk__write[hidden] { display: none; }
/* not display: none, or some browsers lose the filter inside it */
.talk__filter { position: absolute; }
/* no background on the field: this paints the bar, and a fill there
   would lay a hard rectangle over the liquid edge exactly where the
   button is leaving it */
.talk__goo {
    position: absolute;
    inset: 0;
    filter: url(#talkGoo);
    /* WebKit otherwise repaints the goo a frame or two behind the
       plain-DOM content */
    will-change: filter, transform;
    pointer-events: none;
}
/* ── THE BAR AND THE BUTTON TRADE ONE WIDTH ───────────────
   Both states add up to the same outer box. Holding the total
   fixed is not tidiness: a box that changes size re-packs
   everything around it. So the bar gives up exactly what the
   button takes, and the outer box never moves. */
.talk__goo-bar,
.talk__field {
    width: calc(100% - var(--cmd-btn));
    transition: width var(--cmd-trade);
}
.talk__write.is-split :is(.talk__goo-bar, .talk__field) { width: calc(100% - var(--cmd-tuck)); }
.talk__goo-bar {
    position: absolute;
    inset: 0 auto 0 0;
    border-radius: 28px;
    background: var(--fill-slab);
}
.talk__field {
    position: relative;
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 56px;
    /* the right side is padded for the mic rather than for text */
    padding: 10px 10px 10px 20px;
}
.talk__goo-go,
.talk__send {
    position: absolute;
    right: 0;
    /* in the middle of the bar, however many lines it has grown to */
    top: 50%;
    margin-top: calc(var(--cmd-btn) / -2);
    width: var(--cmd-btn);
    height: var(--cmd-btn);
    border-radius: 50%;
    transform: translateX(calc(-1 * var(--cmd-tuck)));
}
.talk__goo-go { background: var(--fill-slab); transition: transform var(--cmd-morph); }
.talk__write.is-split :is(.talk__goo-go, .talk__send) { transform: none; }
.talk__input {
    flex: 1;
    min-width: 0;
    max-height: 132px;
    padding: 7px 0;
    /* it sizes to its words, so a bar here only ever marks a rounding error */
    scrollbar-width: none;
    border: 0;
    outline: 0;
    resize: none;
    background: none;
    font: 500 16px/1.4 var(--font-ui);
    color: var(--wavy);
    caret-color: var(--lavender);
    transition: height .25s var(--ease-out);
}
.talk__input::placeholder { color: rgb(var(--wavy-rgb) / 0.38); }
/* ── THE SAME MATERIAL AS THE BAR ───────────────────────────
   It was an ink disc with a light glyph, which made the thing
   that splits off a different object from the thing it split
   FROM — and the goo's whole claim is that they are one body
   separating. Same fill as the field, ink glyph, and the seam
   between them while they neck is continuous.

   NOTHING AT REST. Tucked inside the bar it is a body for the
   goo to split from and nothing more — no disc, and its glyph
   is hidden too, so what you see is one bar. */
.talk__send {
    display: grid;
    place-items: center;
    background: var(--fill-slab);
    color: var(--wavy);
    opacity: 0;
    /* the press lands on the bar instead, so it keeps the caret */
    pointer-events: none;
    transition:
        transform var(--cmd-morph),
        opacity 160ms ease,
        background-color 220ms ease,
        color 220ms ease,
        scale .6s var(--ease-wavy);
}
.talk__write.is-split .talk__send { opacity: 1; }
/* ── TWO STATES, AND NEITHER HAS AN EDGE ────────────────────
   Focused with an empty field, the button is the same material
   as the bar it just split out of: present and offered, asking
   for nothing. The first character makes it the thing you are
   about to press, and it says so by filling — the only solid
   object in the component, which is what a primary action is. */
.talk__send:not(:disabled) {
    background: var(--wavy);
    color: var(--black);
    pointer-events: auto;
}
.talk__send svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
/* Inside the bar, and it does not travel. Fading is the whole of
   what it does: no scale, no travel — the one thing it does is
   stop being there. */
.talk__mic {
    position: relative;
    flex: none;
    display: grid;
    place-items: center;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    color: rgb(var(--wavy-rgb) / 0.6);
    transition: color .3s, opacity 160ms ease;
}
.talk__mic[hidden] { display: none; }
/* still listening means it still has a job: it is how you stop */
.talk__write.is-split .talk__mic:not(.is-on) { opacity: 0; }
.talk__mic svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.talk__mic:hover { color: var(--wavy); }
/* listening: the hero's colors, breathing round it */
.talk__mic::before {
    content: "";
    position: absolute;
    inset: 3px;
    border-radius: 50%;
    background: conic-gradient(from 40deg, #a98bf0, #f58fc4, #ffd06e, #8fb2f5, #a98bf0);
    filter: blur(6px);
    opacity: 0;
    transition: opacity .4s;
}
.talk__mic svg { position: relative; }
.talk__mic.is-on { color: var(--wavy); }
.talk__mic.is-on::before { opacity: 0.8; animation: orb-glow 3s linear infinite; }
.talk__send:not(:disabled):hover { scale: 1.08; }
@media (prefers-reduced-motion: reduce) {
    .talk__goo-bar, .talk__field, .talk__goo-go, .talk__send { transition: none; }
}
.talk__choices { display: flex; flex-wrap: wrap; gap: 8px; outline: none; }
.talk__choices[hidden] { display: none; }
.talk-chip {
    padding: 11px 16px;
    border-radius: 99px;
    box-shadow: inset 0 0 0 1px rgb(var(--wavy-rgb) / 0.18);
    font: 500 14px/1 var(--font-ui);
    color: var(--wavy);
    transition: background-color .3s, color .3s, box-shadow .3s;
}
.talk-chip:hover { background: var(--wavy); color: var(--black); box-shadow: none; }
.talk__note {
    min-height: 16px;
    padding-left: 20px;
    font: 500 12px/1.3 var(--font-ui);
    color: rgb(var(--wavy-rgb) / 0.4);
}
.talk__note.is-wrong { color: #ff9d8a; }

.talk-heard { display: flex; flex-direction: column; border-top: 1px solid rgb(var(--wavy-rgb) / 0.12); }
.talk-heard div {
    display: grid;
    grid-template-columns: 7.5em minmax(0, 1fr);
    gap: 12px;
    padding: 10px 0;
    border-bottom: 1px solid rgb(var(--wavy-rgb) / 0.12);
    font: 500 14px/1.35 var(--font-ui);
}
.talk-heard dt { color: rgb(var(--wavy-rgb) / 0.5); }
.talk-heard dd { color: var(--wavy); overflow-wrap: anywhere; }
.talk-next { font: 500 15px/1.4 var(--font-ui); color: rgb(var(--wavy-rgb) / 0.6); }
.talk-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.talk-actions .btn { padding-block: 13px; }
.talk-actions .btn--ghost { background: none; border-color: rgb(var(--wavy-rgb) / 0.2); color: var(--wavy); }
.talk-actions .talk-send-it { background: var(--wavy); color: var(--black); }

@media (max-width: 899px) {
    /* fitted to what the keyboard leaves, set from talk.js */
    .talk {
        --talk-gap: 12px;
        inset: auto 0 auto 0;
        top: var(--talk-top, 0px);
        height: var(--talk-vh, 100dvh);
    }
    .talk__panel {
        left: var(--talk-gap);
        top: var(--talk-gap);
        translate: none;
        width: calc(100% - 2 * var(--talk-gap));
        height: calc(100% - 2 * var(--talk-gap));
        grid-template-columns: minmax(0, 1fr);
        grid-template-rows: auto minmax(0, 1fr);
        gap: 4px;
        padding: 8px;
        border-radius: 22px;
    }
    /* three wide by two tall at most, sized from talk.js */
    .talk__stage {
        height: var(--talk-stage, calc((100vw - 40px) * 2 / 3));
        transition: height .4s var(--ease-out), background-color .5s var(--ease-out), box-shadow .5s var(--ease-out);
    }
    .talk__note:empty { display: none; }
    .talk__shape { inset: 8px; }
    .talk__status { display: none; }
    .talk__chat { padding: 0 6px 6px 8px; }
    /* the count, Back and the X sit over the picture, out of the words' way */
    .talk__bar {
        position: absolute;
        top: 8px;
        left: 8px;
        right: 8px;
        z-index: 2;
        padding: 0 8px 0 14px;
    }
    .talk__thread { padding-top: 12px; padding-bottom: 12px; }
    .talk-say { font-size: 24px; }
    .talk.is-tight .talk-say { font-size: 20px; }
    .talk-heard div { grid-template-columns: 6.5em minmax(0, 1fr); padding: 8px 0; }
}

/* ============================================
   LOADING
   ============================================ */
/* hidden, not moved: GSAP reads a CSS transform as its own
   starting offset, so the masks are set from JS instead */
body.is-loading .hero__content { visibility: hidden; }
/* the nav comes in on a fresh load only: arrived at by the page wipe,
   it was there all along, over the panels */
html:not(.is-arriving, .came-by-wipe) body.is-loading .header { visibility: hidden; }
/* an inner page's entrance waits for its script from the first paint
   (enter-waits, set in the head on a fresh load): the nav that slides
   in (nav-in.js), and the hero's summary, label, links and buttons. The
   script sets up their own hidden states and then lets go, so nothing
   paints as plain text first and breaks into lines after. If the
   script never comes, they show */
html.enter-waits :is(.header, [data-cp-lede], [data-cp-cta], [data-ds-lede]) { visibility: hidden; animation: enter-shows 0s 4s forwards; }
@keyframes enter-shows { to { visibility: visible; } }

.sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ══ Loaders (lab/Loaders.tsx) ══════════════════════════════════
   One loader, no tile, all CSS. Wave is the bars from the old
   spinner set; Bounce, Morph, Pixel and Blocks start from
   loading.dev (MIT) and each take a turn of their own. Every
   duration is multiplied by --spd. Nothing to press: each one only
   loops. */
.ldr {
  display: grid;
  place-items: center;
  color: var(--ink);
  --ldr-soft: color-mix(in srgb, var(--ink) 14%, transparent);}

/* flex, not grid: the pixel loader is wider than this 48px slot, and a
   grid item bigger than its cell gives up centring and aligns to its
   start, so it hung 11px right of the middle. Flex centring overflows
   evenly both ways */
.ldr-slot {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;}

/* drawn a fifth smaller than they are built, and never squeezed */
.ldr-slot > * { flex: none; scale: 0.8; }

/* wave: four bars rising and falling one after another */
.ldr-wave { display: flex; align-items: center; gap: 4px; height: 34px; }

.ldr-wave i {
  width: 5px;
  height: 100%;
  border-radius: 3px;
  background: currentColor;
  animation: ldr-bar calc(0.9s * var(--spd)) ease-in-out infinite;}

.ldr-wave i:nth-child(2) { animation-delay: calc(0.1s * var(--spd)); }

.ldr-wave i:nth-child(3) { animation-delay: calc(0.2s * var(--spd)); }

.ldr-wave i:nth-child(4) { animation-delay: calc(0.3s * var(--spd)); }

.ldr[data-square] .ldr-wave i { border-radius: 1px; }

/* bounce: three dots hopping one after another, giving a little
   where they land — rising slows into the top, falling speeds
   into the floor */
.ldr-bounce { display: flex; align-items: flex-end; gap: 8px; height: 24px; padding-bottom: 8px; box-sizing: border-box; }

.ldr-bounce i {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: currentColor;
  transform-origin: 50% 100%;
  animation: ldr-hop calc(1s * var(--spd)) infinite;}

.ldr-bounce i:nth-child(2) { animation-delay: calc(0.14s * var(--spd)); }

.ldr-bounce i:nth-child(3) { animation-delay: calc(0.28s * var(--spd)); }

.ldr[data-square] .ldr-bounce i { border-radius: 2px; }

/* trace: a short dash running round its own faint track, a rounded
   square or a circle.

   It runs at ONE speed, all the way round, for ever. Each lap used
   to ease in and out, and the dash shrank to its shortest at the
   same moment — so once a lap the line all but stopped and pinched
   to a stub. Now the lap is linear, and the dash's length breathes
   on its own slower clock, long to short and back, so it never
   stalls and never pinches where the lap begins. */
.ldr-trace { width: 40px; height: 40px; overflow: visible; rotate: -90deg; }

.ldr-trace :is(rect, circle) { fill: none; stroke-width: 3.5; stroke-linecap: round; }

.ldr-trace :is(rect, circle):first-child { stroke: var(--ldr-soft); }

.ldr-trace :is(rect, circle):last-child {
  stroke: currentColor;
  stroke-dasharray: 22 78;
  animation:
    ldr-lap calc(1.4s * var(--spd)) linear infinite,
    ldr-reach calc(2.1s * var(--spd)) ease-in-out infinite alternate;}

/* morph: a square that turns a quarter as it rounds into a circle,
   rests, and turns on into a square again — each move landing
   with a small give past its mark */
.ldr-morph { display: grid; place-items: center; width: 40px; height: 40px; }

.ldr-morph i {
  width: 24px;
  height: 24px;
  border-radius: 6px;
  background: currentColor;
  animation: ldr-shape calc(2s * var(--spd)) cubic-bezier(0.6, -0.12, 0.3, 1.22) infinite;}

/* pixel: a rectangle of mini-square clusters, six across and three
   down, and a wave cascading through it from the top left — each
   cluster easing up to full and back a step after its neighbour,
   then a short rest before the next wave */
/* 56: two pixels to each unit of the 28 grid, so every square lands
   whole and crisp, and near the weight of the 40px loaders beside it */
.ldr-pixel { width: 56px; height: 56px; overflow: visible; fill: currentColor; shape-rendering: crispEdges; }

/* the circle: a six by six with its corners off, rippling outward */
.ldr-pixel[data-round] { width: 56px; height: 56px; }

.ldr-pixel g g {
  opacity: 0.15;
  animation: ldr-cascade calc(1.8s * var(--spd)) cubic-bezier(0.45, 0, 0.55, 1) infinite;
  animation-delay: calc(var(--d) * 0.085s * var(--spd));}

/* blocks: a grid that dips corner to corner, each cell easing down
   to just over half its size and back, never turning */
.ldr-blocks {
  display: grid;
  grid-template-columns: repeat(3, 9px);
  grid-template-rows: repeat(3, 9px);
  gap: 4px;}

.ldr-blocks i {
  border-radius: 2.5px;
  background: currentColor;
  animation: ldr-sink calc(1.5s * var(--spd)) cubic-bezier(0.65, 0, 0.35, 1) infinite;
  animation-delay: calc(var(--i) * 0.11s * var(--spd));}

/* two by two: bigger cells, the same sink */
.ldr-blocks[data-n="2"] { grid-template-columns: repeat(2, 13px); grid-template-rows: repeat(2, 13px); gap: 4px; }

.ldr-blocks[data-n="2"] i { border-radius: 6px; animation-delay: calc(var(--i) * 0.16s * var(--spd)); }

/* dots: the same sink, round cells */
.ldr-blocks[data-dots] i { border-radius: 50%; }

@keyframes ldr-bar { 0%, 100% { scale: 1 0.3; } 40% { scale: 1 1; } }

@keyframes ldr-hop {
  0%, 100% { translate: 0 0; scale: 1.1 0.9; animation-timing-function: cubic-bezier(0.25, 0.6, 0.4, 1); }
  45% { translate: 0 -9px; scale: 1 1; animation-timing-function: cubic-bezier(0.6, 0, 0.75, 0.4); }}

@keyframes ldr-lap {
  from { stroke-dashoffset: 0; }
  to { stroke-dashoffset: -100; }}

@keyframes ldr-reach {
  from { stroke-dasharray: 16 84; }
  to { stroke-dasharray: 30 70; }}

@keyframes ldr-shape {
  0%, 12% { border-radius: 6px; rotate: 0deg; scale: 1; }
  50%, 62% { border-radius: 12px; rotate: 90deg; scale: 0.9; }
  100% { border-radius: 6px; rotate: 180deg; scale: 1; }}

@keyframes ldr-cascade {
  0%, 60%, 100% { opacity: 0.15; }
  22% { opacity: 1; }}

@keyframes ldr-sink {
  0%, 60%, 100% { scale: 1; }
  30% { scale: 0.55; }}

/* wavy: a loader rests until it's played */
.ldr[data-rest] * { animation-play-state: paused; }
/* wavy: the morph as a capability's mark, at 12px and set against the
   left edge, where the row's number was. This small its pixel corners
   would read as a circle throughout, so it rounds by the share, as the
   menu's marks do */
.cap__mark .ldr { justify-items: start; color: inherit; }
.cap__mark .ldr-slot { width: 0.75rem; height: 0.75rem; }
.cap__mark .ldr-slot > * { scale: 1; }
.cap__mark :is(.ldr-morph, .ldr-morph i) { width: 0.75rem; height: 0.75rem; }
.cap__mark .ldr-morph i { border-radius: 25%; animation-name: ldr-shape-small; }
