/* tastemaker · macrostructure: Long-Scroll Narrative · mood: technical, cinematic dial-up · site: COC Bot by Raboy (Flagship: Console + Night Run)
 * arc: hook(H2 split demo) -> solution(F4 stations 01-07) -> safety(F4 station 08, double dwell) -> proof(P4 live recorded progress + P2 quote) -> close(C2 + Ft2); no problem beat, the page is a product tour
 * nav: N2 · hero: H2 · footer: Ft2 · knobs: nav=product-bar/4-links/filled-action; hero=xl/left-bias/genuine client render; features=F4/sideways track, timeline ruler at the foot; proof=pinned two-beat data stage; head=S2
 * palette: user-specified client tokens (dark, no seed) · contrast: pass (matrix in .tastemaker/style-lock.md) */

/* Fonts: the client's own IBM Plex files. Sans carries everything; Mono is for numerals, readouts and short labels. */
@font-face { font-family: 'IBM Plex Sans'; font-weight: 400; font-style: normal; font-display: swap; src: url('plex-regular.7bd3801295ef.woff2') format('woff2'); }
@font-face { font-family: 'IBM Plex Sans'; font-weight: 500; font-style: normal; font-display: swap; src: url('plex-medium.30ee00b1a983.woff2') format('woff2'); }
@font-face { font-family: 'IBM Plex Sans'; font-weight: 700; font-style: normal; font-display: swap; src: url('plex-bold.494c9643824a.woff2') format('woff2'); }
@font-face { font-family: 'IBM Plex Mono'; font-weight: 400; font-style: normal; font-display: swap; src: url('plex-mono.2e354a5aab45.woff2') format('woff2'); }

:root {
  color-scheme: dark;
  /* Locked palette: the client's theme tokens. No other colour enters the build, except the three rank
     colours scoped to the Rankings stop (rankings.css), an exception the owner approved for it alone. */
  --canvas: #0F141B; --raised: #171D26; --raised-high: #1D2632; --inset: #0B1016;
  --outline: #34404F; --outline-soft: #283340;
  --text: #F3F5F7; --text-secondary: #B5BDC9; --text-muted: #8B95A5;
  --gold: #D6B15E; --success: #68C79B; --warning: #E7A85F; --error: #EE8790; --info: #81AFE8;
  /* Derived states: mixes of two tokens only. */
  --gold-hover: color-mix(in srgb, var(--gold) 86%, var(--text));
  --gold-track: color-mix(in srgb, var(--gold) 50%, var(--raised));
  --hover-wash: color-mix(in srgb, var(--text) 6%, transparent);
  --shadow-overlay: 10px 14px 30px color-mix(in srgb, var(--inset) 72%, transparent);
  --font-sans: 'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace;
  /* Client shape language. */
  --r-focus: 4px; --r-tip: 8px; --r-control: 10px; --r-card: 16px; --r-pill: 999px;
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  /* Spring: things that change state land with a little carried momentum and settle; nothing bounces twice.
     This curve is the fallback for browsers without linear() easing; the measured one replaces it below. */
  --spring: cubic-bezier(0.3, 1.36, 0.5, 1);
  --spring-measured: linear(0, 0.09 5%, 0.286 10%, 0.505 15%, 0.701 20%, 0.852 25%, 0.955 30%, 1.017 35%, 1.046 40%, 1.054 45%, 1.05 50%, 1.039 55%, 1.027 60%, 1.016 65%, 1.008 70%, 1.003 75%, 0.999 80%, 0.998 85%, 0.997 90%, 0.997 95%, 1);
  /* Stage geometry. --axis is where the playhead sits; all station copy hangs off it. */
  --bar: 56px;
  /* Page gutter: the space between the viewport edge and the page column. */
  --gutter: 16px;
  --axis: clamp(36px, 3.6vw, 56px);
  --pad: 28px;
  --top: clamp(84px, 11.5vh, 104px);
  --bottom: clamp(88px, 11vh, 96px);
  /* The session track rests along the foot of the stage, like a timeline. */
  --ruler-y: calc(100% - 46px);
  /* Every scroll-driven figure reads --p (0 to 1). The default is the finished state, so nothing depends on script. */
  --p: 1;
  /* Roomier stages show the recreations larger (set in client.css). Anything that must match their width reads it. */
  --zoom: 1;
}
/* The step response of a damped spring (damping 0.68, about 5% overshoot). An unsupported easing inside a
   custom property would silently switch every transition off, so it is only adopted where it parses. */
@supports (transition-timing-function: linear(0, 1)) { :root { --spring: var(--spring-measured); } }
/* Short windows (a 1366 x 768 laptop leaves about 650px): the stage gives its margins back to the stations. */
@media (min-width: 900px) { :root { --gutter: 40px; } }
@media (max-height: 719px) { :root { --bar: 48px; --top: 60px; --bottom: 80px; --ruler-y: calc(100% - 38px); } }

*, *::before, *::after { box-sizing: border-box; }
/* Both clip: the root value becomes the viewport's, so the body's own clip is what keeps a stray wide element from
   stretching the layout viewport on a phone. */
html { -webkit-text-size-adjust: 100%; scroll-behavior: auto; overflow-x: clip; }
body { margin: 0; background: var(--canvas); color: var(--text); font: 400 17px/1.5 var(--font-sans); overflow-x: clip; -webkit-font-smoothing: antialiased; }
h1, h2, h3, p, dl, dd, ol, ul, figure { margin: 0; }
ol, ul { padding: 0; list-style: none; }
img, svg, canvas { display: block; }
em { font-style: normal; }
.icon { flex: none; width: 18px; height: 18px; }
a { color: inherit; }
button { font: inherit; color: inherit; }
[hidden] { display: none !important; }

/* Focus is instant and always gold. Headings only take programmatic focus after a jump, so they show no ring. */
:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; border-radius: var(--r-focus); }
[tabindex="-1"]:focus { outline: none; }

.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.sprite { position: absolute; }
.skip { position: fixed; left: 12px; top: 12px; z-index: 20; padding: 12px 16px; border-radius: var(--r-control); background: var(--gold); color: var(--inset); font-weight: 700; font-size: 14px; text-decoration: none; transform: translateY(-200%); }
.skip:focus-visible { transform: none; outline-offset: 2px; }

/* The one filled action. Dark label on gold: light text on gold fails contrast. */
.btn { display: inline-flex; align-items: center; gap: 10px; min-height: 48px; padding: 0 20px 0 16px; border-radius: var(--r-control); background: var(--gold); color: var(--inset); font-weight: 700; font-size: 15px; line-height: 1; text-decoration: none; white-space: nowrap; transition: transform 120ms var(--ease-out); }
.btn:active { transform: scale(0.975); }
.btn--bar { min-height: 40px; padding: 0 16px; font-size: 14px; }
.link { display: inline-flex; align-items: center; gap: 6px; min-height: 44px; color: var(--text); font-size: 15px; font-weight: 500; text-decoration: underline; text-decoration-color: var(--outline); text-underline-offset: 5px; white-space: nowrap; }
.link svg { transition: transform 150ms var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .btn:hover { background: var(--gold-hover); }
  .link:hover { text-decoration-color: var(--text); }
  .link:hover svg { transform: translateX(3px); }
}

/* Short technical labels: station numbers, figure labels, readouts. */
.num, figcaption, .point-note, .plat, .soon, .ruler-cue { font-family: var(--font-mono); font-weight: 400; }
figcaption, .point-note { margin-top: 10px; color: var(--text-muted); font-size: 12px; line-height: 1.4; letter-spacing: 0.01em; }

/* Ambient layer: one ordered-dither field behind everything. Each canvas pixel is one dither cell.
   The canvas lives inside main (it is home-only content), so it sits below everything else in main's own stack. */
.field { position: fixed; left: 0; top: 0; z-index: -1; pointer-events: none; image-rendering: pixelated; }
main, .foot { position: relative; z-index: 1; }


/* Navigation: one product bar and a one-line footer, shared by every page.
   Inner pages line both up with the page column. On the home journey the bar is fixed over the stage and lines up
   with the station copy instead. */
.bar { position: relative; z-index: 5; display: flex; flex-wrap: wrap; align-items: center; gap: 0 20px; min-height: var(--bar); padding: 4px max(var(--gutter), calc((100% - 1240px) / 2)); background: var(--canvas); border-bottom: 1px solid var(--outline-soft); }
.brand { display: inline-flex; align-items: center; gap: 10px; min-height: 44px; margin-right: auto; color: var(--text); font-size: 17px; font-weight: 500; text-decoration: none; white-space: nowrap; }
.brand small { margin-left: 2px; color: var(--text-muted); font-size: 0.7em; font-weight: 400; }
.site { display: flex; flex-wrap: wrap; gap: 0 22px; order: 3; flex-basis: 100%; border-top: 1px solid var(--outline-soft); }
.site a { display: inline-flex; align-items: center; min-height: 44px; color: var(--text-secondary); font-size: 14px; font-weight: 500; text-decoration: none; }
.site a[aria-current] { color: var(--text); }
@media (hover: hover) and (pointer: fine) { .site a:hover, .foot-nav a:hover { color: var(--text); } }
/* A phone keeps the wordmark and the action on one row. */
@media (max-width: 479px) { .bar { gap: 0 12px; } .btn--bar { padding: 0 12px; font-size: 13px; } .site { gap: 0 14px; } .site a { font-size: 13px; } }
@media (min-width: 900px) {
  .bar { position: sticky; top: 0; }
  .site { order: 0; flex-basis: auto; margin-right: 12px; border: 0; }
}
/* The bar is clear while the stage is under it and turns solid once the track has released. */
.run .bar { position: fixed; inset: 0 0 auto 0; height: var(--bar); padding: 0 40px 0 calc(var(--axis) + var(--pad)); background: transparent; border-bottom-color: transparent; transition: background-color 180ms linear, border-color 180ms linear; }
.run.is-done .bar { background: color-mix(in srgb, var(--canvas) 88%, transparent); border-bottom-color: var(--outline-soft); -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px); }

/* ---------- Footer ---------- */
.foot { position: relative; z-index: 1; display: grid; gap: 6px; padding: 28px max(var(--gutter), calc((100% - 1240px) / 2)) 64px; background: var(--canvas); border-top: 1px solid var(--outline-soft); font-size: 14px; }
.foot-line { display: flex; flex-wrap: wrap; align-items: center; gap: 0 24px; color: var(--text-secondary); }
.foot-line .brand { font-size: 15px; }
.foot-nav { display: flex; flex-wrap: wrap; gap: 0 22px; }
.foot-nav a { display: inline-flex; align-items: center; min-height: 44px; color: var(--text-secondary); text-decoration: none; }
.foot-legal { max-width: 68ch; color: var(--text-muted); font-size: 13px; line-height: 1.5; }
/* On the home page everything after the track lines up with the station copy. */
.home-page .foot { padding-left: 44px; }
@media (min-width: 900px) { .home-page .foot { padding-left: 84px; } }
.run .foot { padding-left: calc(var(--axis) + var(--pad)); }

/* The run: stage, playhead, ruler and station frame. The product bar lives in navigation.css.
   Base rules are the stacked page (no script, or a small viewport). Rules under .run are the sideways journey. */

/* ---------- Stage ---------- */
.stage { position: relative; display: flex; flex-direction: column; }
/* Stacked: the track turns vertical and runs down the left edge; the gold tick is the playhead, fixed while stations pass. */
.stage::before { content: ''; position: absolute; z-index: 1; left: 19px; top: 0; bottom: 0; width: 1px; background: var(--outline-soft); }
.lens, .world { display: contents; }
.playhead { order: -1; position: sticky; top: 40vh; z-index: 2; height: 0; pointer-events: none; }
.ph-line { display: none; }
.ph-head { position: absolute; left: 13px; top: 0; width: 13px; height: 2px; background: var(--gold); }
/* The stretch already travelled is gold: a line that hangs above the playhead and travels with it. It needs no
   script, and the bar covers the part that would reach above the stage at the top of the page. */
.playhead::before { content: ''; position: absolute; left: 19px; bottom: 0; width: 1px; height: 40vh; background: var(--gold); transform-origin: top; }

.run .track { height: calc((var(--span, 10.32) + 1) * 100dvh); }
.run .stage { position: sticky; top: 0; display: block; height: 100dvh; overflow: clip; }
.run .stage::before { display: none; }
/* Edge fades: stations dissolve as they pass left of the playhead and arrive softly from the right.
   While the hero rests the right fade is longer (--fade, written by journey.js), so the panel beyond the edge
   reads as distance, not as a cut. The property is registered as one that does not inherit: every element under
   a changed inherited property is restyled, and the lens holds the whole journey. */
@property --fade { syntax: '<length>'; inherits: false; initial-value: 40px; }
.run .lens { display: block; position: absolute; inset: 0; transition: opacity 220ms linear; -webkit-mask-image: linear-gradient(to right, transparent, var(--inset) calc(var(--axis) - 6px), var(--inset) calc(100% - var(--fade, 40px)), transparent); mask-image: linear-gradient(to right, transparent, var(--inset) calc(var(--axis) - 6px), var(--inset) calc(100% - var(--fade, 40px)), transparent); }
.run .world { display: flex; position: absolute; inset: 0 auto 0 0; will-change: transform; }
.run .playhead { position: absolute; left: var(--axis); top: 0; bottom: 0; z-index: 4; width: 1px; height: auto; }
.run .playhead::before { display: none; }
.run .ph-line { display: block; position: absolute; inset: 0; background: var(--gold); transform-origin: top; }
.run .ph-head { left: -4px; top: calc(var(--ruler-y) - 9px); width: 9px; height: 7px; clip-path: polygon(0 0, 100% 0, 50% 100%); }

/* ---------- Ruler: the session track. Stacked it is a route strip under the hero. ---------- */
.ruler { order: 1; position: relative; height: 56px; margin-left: 44px; overflow: hidden; }
.ruler-line { position: absolute; left: 0; right: 0; top: 30px; height: 1px; background: var(--outline); transform-origin: left; }
.ticks { display: flex; }
.tick { position: relative; flex: 1; height: 56px; }
.tick::after { content: ''; position: absolute; left: 0; top: 30px; width: 1px; height: 8px; background: var(--text-muted); }
.tick a { display: block; height: 100%; color: var(--text-muted); text-decoration: none; }
.tick-n { display: block; padding-top: 8px; font: 400 11px/1 var(--font-mono); letter-spacing: 0.04em; }
.tick-name, .tick-band, .ruler-cue { display: none; }
.tick--key .tick-n { color: var(--text); }
.tick--key::after { height: 14px; background: var(--text); }
.tick--soon::after { width: 0; border-left: 1px dashed var(--text-muted); background: none; }

.run .ruler { position: absolute; left: var(--axis); top: var(--ruler-y); z-index: 3; width: calc(100% - var(--axis) - 40px); height: 0; margin: 0; overflow: visible; will-change: transform; }
.run .ruler-line { left: -20vw; right: -220vw; top: 0; }
/* Minor marks every tenth of a screen of scroll: the ruler is a real time axis, so a long dwell reads as a long stretch. */
.run .ruler::after { content: ''; position: absolute; left: 0; right: -220vw; top: 1px; height: 4px; background: repeating-linear-gradient(to right, var(--outline) 0 1px, transparent 1px var(--minor, 14px)); }
.run .ticks { display: block; width: auto; }
.run .tick { position: absolute; top: 0; flex: none; width: 0; height: 0; }
.run .tick::after { top: 0; height: 10px; }
.run .tick a { position: absolute; left: -22px; top: -40px; width: 44px; height: 52px; color: var(--text-secondary); text-align: center; }
/* Numbers sit to the right of their mark, ruler fashion, so the playhead never strikes through the live one. */
.run .tick-n { padding-top: 13px; font-size: 13px; transform: translateX(14px); }
/* The dwell is drawn as a band leading up to each tick: the longer the stay, the longer the band. */
.run .tick-band { display: block; position: absolute; right: 0; top: -1px; width: calc(var(--band, 0) * var(--ruler-w, 0px)); height: 3px; background: var(--text-muted); }
/* Station names sit under the track in two alternating rows while it rests in the hero, then leave as it lifts. */
.run .tick-name { left: 50%; top: 58px; }
.run .tick-name, .run .ruler-cue { transition: opacity 220ms linear, visibility 0s; }
.run .tick:nth-child(even) .tick-name { top: 80px; }
.run .stage:not(.is-up) .tick:nth-child(even)::after { height: 32px; }
.run .ruler-cue { display: inline-flex; align-items: center; gap: 6px; position: absolute; left: 14px; top: 40px; color: var(--text-muted); font-size: 12px; line-height: 1; letter-spacing: 0.04em; }
/* They leave as the track starts to lift out of the hero. */
.run .stage.is-up .tick-name, .run .stage.is-up .ruler-cue { opacity: 0; visibility: hidden; transition: opacity 220ms linear, visibility 0s linear 220ms; }
/* The number under the playhead steps forward and settles. */
.run .tick-n { transition: transform 460ms var(--spring), color 160ms linear; transform-origin: left center; }
.run .tick.is-live .tick-n { color: var(--text); transform: translateX(14px) scale(1.22); }
.run .tick.is-past .tick-n { color: var(--text-muted); }
.run .tick--key .tick-band { height: 5px; top: -2px; background: var(--text); }
.run .tick--key .tick-name { color: var(--text); font-weight: 500; }
.run .tick--key::after { height: 16px; }
.run .tick--soon .tick-band { height: 0; border-top: 1px dashed var(--text-muted); background: none; top: 0; }
.run .tick--soon .tick-n, .run .tick--soon .tick-name { color: var(--text-muted); }

/* ---------- Station frame ---------- */
.station { order: 2; position: relative; scroll-margin-top: 64px; display: grid; gap: 28px; align-content: start; min-width: 0; padding: 56px 16px 56px 44px; }
.station > * { min-width: 0; }
.station::before { content: ''; position: absolute; left: 13px; top: 64px; width: 13px; height: 1px; background: var(--outline); }
.station.hero { order: 0; padding-top: 36px; padding-bottom: 24px; }
.station.hero::before { display: none; }
.station.is-soon { padding-block: 20px; }
.station.is-soon::before { top: 28px; width: 0; height: 13px; left: 19px; border-left: 1px dashed var(--outline); background: none; }

.cap h2 { font: 500 clamp(27px, 2.7vw, 40px)/1.1 var(--font-sans); letter-spacing: -0.015em; text-wrap: balance; }
/* The station number types on. The clip reaches left of the number so the hairline that ties it to the playhead stays whole. */
.num { display: block; margin-bottom: 14px; color: var(--text-muted); font-size: 13px; line-height: 1; letter-spacing: 0.06em; clip-path: inset(-2px calc((1 - var(--nin, 1)) * 100%) -2px -90px); transition: clip-path 380ms steps(3, end) var(--kd, 0ms); }
.rv-wait .num { --nin: 0; }
.cap-line { margin-top: 14px; max-width: 30em; color: var(--text-secondary); text-wrap: pretty; }

@media (min-width: 900px) {
  /* Wide but not on the journey (no script, or a window too small for it): caption and figure sit two-up. */
  .station { padding: 88px 40px 88px 84px; }
  .station::before { top: 96px; left: 20px; width: 44px; }
  .stage::before, .playhead::before { left: 20px; }
  .ph-head { left: 14px; }
  .station--split, .station--half { grid-template-columns: minmax(0, 0.7fr) minmax(0, 1.25fr); gap: 56px; align-items: center; }
  .ruler { height: 104px; margin-left: 84px; }
  .tick { height: 104px; }
  /* Names are centred on their mark, so a long one never runs into the next mark's stem. */
  .tick-name { display: block; position: absolute; left: 0; top: 50px; color: var(--text-secondary); font: 400 13px/1 var(--font-sans); white-space: nowrap; transform: translateX(-50%); }
  .tick:nth-child(even) .tick-name { top: 72px; }
  html:not(.run) .tick:nth-child(even)::after { height: 36px; }
}

.run .station { flex: none; width: var(--stage-w, 100vw); height: 100%; align-content: center; padding: var(--top) 40px var(--bottom) calc(var(--axis) + var(--pad)); }
.run .station::before { display: none; }
/* Each station number is tied back to the playhead by a short hairline. */
.run .num { position: relative; }
.run .num::before { content: ''; position: absolute; right: calc(100% + 8px); top: 50%; width: calc(var(--pad) - 8px); height: 1px; background: var(--outline); }
/* Every full station is exactly one stage wide; only the columns inside are capped. */
.run .station--split { grid-template-columns: minmax(280px, 400px) minmax(0, 820px); gap: clamp(40px, 5vw, 80px); justify-content: start; align-items: center; }
.run .station--stack { gap: clamp(16px, 2.6vh, 28px); }
/* Compact and unlit stations are half a stage wide, so the journey passes them without a full stop. */
.run .station--half, .run .station.is-soon { width: calc(var(--stage-w, 100vw) * 0.5); grid-template-columns: minmax(0, 1fr); gap: 28px; padding-right: 0; }
.run .station--half .fig { max-width: 460px; }

/* Short windows have no room for a caption above the session list: that station goes side by side as well. */
@media (max-height: 799px) {
  .run .station--tall { grid-template-columns: minmax(280px, 400px) minmax(0, 820px); gap: clamp(40px, 5vw, 80px); justify-content: start; align-items: center; }
}
/* The shortest windows keep the numbered track in the hero and drop its two rows of names. */
@media (max-height: 639px) {
  .run .tick-name, .run .ruler-cue { display: none; }
  .run .stage:not(.is-up) .tick:nth-child(even)::after { height: 10px; }
}

@media (max-height: 719px) { .run .num { margin-bottom: 8px; } }

/* Station content: hero copy, how each figure is composed, the recorded readouts, the two diagrams and the unlit stations.
   The client components themselves live in client.css. */

/* ---------- Station 00: the hook ---------- */
.hero h1 { max-width: 12.6em; font: 500 clamp(34px, 3.75vw, 72px)/1.05 var(--font-sans); letter-spacing: -0.022em; text-wrap: balance; }
/* The last word takes its own line: the sentence lands, then the kicker. */
.hero h1 .land { display: block; }
/* The search phrase for the page sits above the headline as a quiet label. */
.hero h1 small { display: block; margin-bottom: 18px; color: var(--text-muted); font: 400 13px/1.2 var(--font-mono); letter-spacing: 0.04em; }
.hero-sub { max-width: 31em; color: var(--text-secondary); text-wrap: pretty; }
.hero-sub a { color: var(--text); text-decoration: underline; text-decoration-color: var(--text-muted); text-underline-offset: 4px; }
.hero-act { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px; }
.plat { color: var(--text-muted); font-size: 12px; letter-spacing: 0.04em; }
.hero-copy { display: grid; gap: 20px; align-content: center; min-width: 0; }
.hero-copy .hero-act { margin-top: 8px; }
@media (hover: hover) and (pointer: fine) { .hero-sub a:hover { text-decoration-color: var(--text); } }

/* The genuine client render: natural size, clipped, never scaled, and only where the journey gives it room.
   Each frame height ends the crop in a gap between controls, so nothing is sliced in half. */
.hero-shot { display: none; min-width: 0; }
.run .hero-shot { display: block; }
.hero-shot-frame { height: 455px; overflow: hidden; border: 1px solid var(--outline); border-right: 0; border-radius: var(--r-card) 0 0 var(--r-card); background: var(--canvas); }
.hero-shot-frame img { width: 1200px; max-width: none; height: 900px; }
@media (max-height: 759px) { .hero-shot-frame { height: 250px; } }
/* On the journey the hero is one full stage: the promise beside the product. The track rests below both. */
.run .station.hero { grid-template-columns: minmax(0, 0.82fr) minmax(0, 1fr); gap: clamp(32px, 4vw, 64px); align-items: center; padding-right: 0; padding-bottom: calc(var(--bottom) + 72px); }
@media (max-height: 719px) { .run .hero h1 { font-size: clamp(34px, min(3.75vw, 8.4vh), 72px); } }
/* ---------- Figures ---------- */
/* Weight: while the camera travels, journey.js shifts the figures against it and the copy by less, so the two
   read as different depths. This transition is the follow-through: when the camera stops they ease back and settle. */
.run .station > :is(.fig, .duo, .points, .risk, .hero-shot, .cap, .hero-copy) { transition: transform 720ms var(--spring); }
/* On the journey a station assembles as it arrives: the number types on, the title rises word by word, then
   each supporting line settles in. All of it hangs off --p, so leaving backwards takes it apart again.
   Stacked, the same parts arrive once as they enter the viewport (behavior/reveal.js). */
.run .station:not(.hero) .num { --nin: clamp(0, calc(var(--p) * 300), 1); }
.run .station:not(.hero) [data-kin] .w > span { --in: clamp(0, calc((var(--p) - 0.03) * 200), 1); }
.run .station:not(.hero) .cap > :not(h2) { --on: clamp(0, calc((var(--p) - var(--c, 0.08)) * 200), 1); }
.cap > :nth-child(3) { --c: 0.11; } .cap > :nth-child(4) { --c: 0.14; } .cap > :nth-child(5) { --c: 0.17; }
.run .station:not(.hero) .cap > :not(h2), .duo > .readout { opacity: var(--on); transform: translateY(calc((1 - var(--on)) * 18px)); transition: transform 760ms var(--spring), opacity 280ms linear; }
/* A figure is a size container: the recreations inside it reflow by the room they are given, not by the viewport. */
.fig { min-width: 0; container: fig / inline-size; }

/* 01: the narrow village selector first, the thresholds beside it. 03: settings beside the two live status panels. */
.fig .ui--pair { width: min(100%, 960px); }
.fig .ui--side { width: min(100%, 820px); }
.fig .ui--status { width: min(100%, 640px); }
@container fig (min-width: 600px) {
  .ui--pair { grid-template-columns: minmax(0, 0.56fr) minmax(0, 1fr); }
  .ui--side { grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); }
  .ui--side .ui-col { grid-auto-rows: 1fr; align-content: stretch; }
}

/* 04: the Sessions screen is a list. A phone shows two profiles, a tablet three; on the journey the list runs on
   past the bottom of its window, so the lower edge fades instead of cutting a card in half. */
.fig .ui--sessions { width: min(100%, 900px); }
.fig--sessions .ui-list > :nth-child(n+3) { display: none; }
@media (min-width: 600px) { .fig--sessions .ui-list > :nth-child(3) { display: block; } }
.run .fig--sessions { --list-h: calc(100dvh - var(--top) - var(--bottom) - 250px); }
.run .fig--sessions .ui-list { max-height: var(--list-h); overflow: hidden; -webkit-mask-image: linear-gradient(to bottom, var(--inset) calc(100% - 64px), transparent); mask-image: linear-gradient(to bottom, var(--inset) calc(100% - 64px), transparent); }
.run .fig--sessions .ui-list > .ui-card { display: block; }
@media (max-height: 799px) { .run .fig--sessions { --list-h: calc(100dvh - var(--top) - var(--bottom) - 92px); } }

/* 05, 06, 11: a recreation paired with one more thing to look at, side by side or stacked. */
.duo { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 24px 44px; min-width: 0; }
.duo > .fig { flex: 1 1 340px; max-width: 520px; }
.duo > .readout { flex: 0 1 270px; }
.duo--stack { flex-direction: column; flex-wrap: nowrap; align-items: stretch; gap: 26px; }
.duo--stack > .fig, .duo--stack > .readout { flex: none; max-width: none; }

/* ---------- Recorded readouts: live community totals, filled by progress.js. A dash until then; nothing counts up. ---------- */
/* --on is the readout coming into play: with its station on the journey, with its own stretch of scroll when it
   stands alone on a phone, or when reveal.js lets it in. The figure rises through a mask; its value never animates. */
.readout { --on: clamp(0, calc((var(--p) - var(--c, 0.1)) * 200), 1); display: grid; gap: 6px; align-content: start; }
.readout.rv-wait, .rv-wait .readout { --on: 0; }
.readout strong { clip-path: inset(-0.08em -0.2em -0.14em); font: 400 clamp(44px, 6vw, 96px)/0.95 var(--font-mono); letter-spacing: -0.045em; font-variant-numeric: tabular-nums; white-space: nowrap; }
.readout strong > span { display: inline-block; transform: translateY(calc((1 - var(--on)) * 112%)); transition: transform 900ms var(--ease-out) calc(var(--kd, 0ms) + 60ms); }
.readout > span { color: var(--text-secondary); font-size: 15px; line-height: 1.35; }
.readout small, .readout-stamp { color: var(--text-muted); font: 400 12px/1.4 var(--font-mono); letter-spacing: 0.01em; }
/* A readout that belongs to a caption: smaller, and beside the caption where the station is one wide column. */
.readout--cap { margin-top: 22px; }
.readout--cap strong { font-size: clamp(30px, 3vw, 44px); }
/* Two totals that belong together (both villages' walls) share one stamp. */
.readout-pair { display: flex; flex-wrap: wrap; gap: 8px 36px; margin-top: 22px; }
.readout-pair .readout { flex: 1 1 150px; margin-top: 0; }
.readout-stamp { flex-basis: 100%; }
@media (min-width: 720px) {
  .cap--wide { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 48px; align-items: end; }
  .cap--wide > .readout { grid-column: 2; grid-row: 1 / span 2; margin-top: 0; justify-items: end; text-align: right; }
}
@media (min-width: 600px) {
  .duo--stack .readout { grid-template-columns: auto minmax(0, 1fr); align-items: end; gap: 4px 22px; }
  .duo--stack .readout strong { grid-row: span 2; }
}

/* ---------- Station 02: walls ---------- */
.typ { display: flex; flex-wrap: wrap; gap: 20px 44px; margin-top: 26px; }
.typ dt { color: var(--text-muted); font-size: 13px; }
.typ dd { margin-top: 6px; font: 400 clamp(24px, 2.3vw, 32px)/1.1 var(--font-mono); letter-spacing: -0.02em; white-space: nowrap; }
.typ-note { margin-top: 20px; max-width: 30em; color: var(--text-secondary); font-size: 15px; text-wrap: pretty; }
.walls svg { width: min(100%, 400px); height: auto; aspect-ratio: 1; }
.walls-base { fill: var(--outline); }
/* Each batch switches on when scroll progress passes its threshold: twelve groups of uneven size. */
.batch { fill: var(--text-secondary); opacity: clamp(0, calc((var(--p) - var(--t)) * 200), 1); transition: opacity 320ms linear; }
.b1 { --t: 0.06; } .b2 { --t: 0.13; } .b3 { --t: 0.20; } .b4 { --t: 0.27; } .b5 { --t: 0.34; } .b6 { --t: 0.41; }
.b7 { --t: 0.48; } .b8 { --t: 0.55; } .b9 { --t: 0.62; } .b10 { --t: 0.69; } .b11 { --t: 0.76; } .b12 { --t: 0.83; }
.run .walls svg { width: min(100%, 420px, 100dvh - var(--top) - var(--bottom) - 60px); }

/* ---------- Station 11: the default cycle, drawn to scale. A diagram, not the interface. ---------- */
.cycle { min-width: 0; max-width: calc(640px * var(--zoom)); }
.cycle svg { width: 100%; height: 22px; overflow: visible; }
.cy-work { fill: var(--text-secondary); }
.cy-rest { fill: none; stroke: var(--text-muted); stroke-dasharray: 3 3; vector-effect: non-scaling-stroke; }
/* The cycle is laid down block by block, then the marker crosses it with scroll progress. */
.cycle rect:not(.cy-head) { --up: clamp(0, calc((var(--p) - var(--a, 0.02)) * 200), 1); transform: scaleX(var(--up)); transform-box: fill-box; transform-origin: left; transition: transform 560ms var(--ease-out); }
.cycle rect:nth-child(2) { --a: 0.06; } .cycle rect:nth-child(3) { --a: 0.1; } .cycle rect:nth-child(4) { --a: 0.14; } .cycle rect:nth-child(5) { --a: 0.18; }
/* Stacked, the blocks are laid down when the strip enters, one after another in time. */
html:not(.run) .cycle rect:not(.cy-head) { --up: 1; transition-delay: calc((var(--a, 0.02) - 0.02) * 4000ms); }
html:not(.run) .cycle.rv-wait rect:not(.cy-head) { --up: 0; }
/* The marker crosses the cycle with scroll progress. */
.cy-head { fill: var(--text); transform: translateX(calc(var(--p) * 558px)); }
html:not(.run) .cy-head { transition: transform 160ms linear; }
.cycle-key { display: flex; gap: 22px; margin-top: 12px; color: var(--text-secondary); font-size: 13px; line-height: 1; }
.cycle-key span { display: inline-flex; align-items: center; gap: 8px; }
.cycle-key span::before { content: ''; width: 16px; height: 10px; border-radius: 2px; background: var(--text-secondary); }
.cycle-key span:last-child::before { background: none; border: 1px dashed var(--text-muted); }

/* ---------- Stations 09 and 10: named, unlit, nothing to show ---------- */
.is-soon .cap h2 { color: var(--text-muted); font-size: clamp(20px, 1.9vw, 26px); letter-spacing: -0.01em; }
.soon { margin-top: 12px; color: var(--text-muted); font-size: 12px; letter-spacing: 0.06em; }
.run .is-soon .cap { padding: 10px 0 10px 22px; border-left: 1px dashed var(--outline); }
.run .is-soon .num::before { display: none; }

/* A caption's way through to the matching part of the feature catalogue. */
.cap-more { margin-top: 10px; color: var(--text-secondary); font-size: 14px; }

/* Interface recreations: the client's own components, rebuilt from client/lib/app/theme.dart and surfaces.dart.
   Flat fills, 1px outlines, 16px panels, gold panel titles, rows of label + muted description + gold switch.
   Nothing here is interactive; each recreation is exposed to assistive technology as one labelled image.
   Every recreation is a size container, so its parts reflow by the room they have. */

.ui { display: grid; gap: 8px; width: min(100%, 540px); color: var(--text); font-family: var(--font-sans); text-align: left; container: ui / inline-size; }
.ui-panel { padding: 15px; border: 1px solid var(--outline); border-radius: var(--r-card); background: var(--raised); }
.ui-title { margin-bottom: 10px; color: var(--gold); font-size: 18px; font-weight: 700; line-height: 1.3; }
/* The one-sentence summary the client prints at the top of a settings panel. */
.ui-lead { margin-bottom: 12px; font-size: 15px; line-height: 1.5; text-wrap: pretty; }
.ui-body { color: var(--text-secondary); font-size: 14px; line-height: 1.45; }
.ui-body + .ui-body, .ui-body + .ui-bar { margin-top: 8px; }
/* Status line: bold key, regular value. */
.ui-kv { font-size: 14px; line-height: 1.45; }
.ui-kv b { font-weight: 700; }
.ui-col { display: grid; gap: 8px; align-content: start; }
@container fig (min-width: 420px) and (max-width: 599.98px) { .ui-col { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* Assembly: as a figure comes into play its panels rise into place one after another, and only then do the
   controls start to move (the first switch threshold is 0.12). --up runs 0 to 1 per part; the spring seats it. */
.ui > :not(.ui-col, .ui-list), .ui-col > *, .ui-list > * { --up: clamp(0, calc((var(--p) - var(--a, 0.015)) * 200), 1); opacity: var(--up); transform: translateY(calc((1 - var(--up)) * 22px)); transition: transform 720ms var(--spring), opacity 220ms linear; }
.ui > :nth-child(2), .ui-col > :nth-child(1), .ui-list > :nth-child(1) { --a: 0.04; }
.ui-col > :nth-child(2), .ui-list > :nth-child(2) { --a: 0.065; }
.ui-list > :nth-child(3) { --a: 0.09; }
.ui-list > :nth-child(4) { --a: 0.115; }
/* Stacked, a part arrives once as it enters (reveal.js); its own --p then only runs its controls. */
html:not(.run) :is(.ui > :not(.ui-col, .ui-list), .ui-col > *, .ui-list > *) { --up: 1; }

/* Switch row: the client's signature settings pattern. */
.ui-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 7px 0; }
.ui-row > :first-child { min-width: 0; }
.ui-label { font-size: 13px; line-height: 1.4; }
.ui-desc { color: var(--text-muted); font-size: 12px; line-height: 1.4; }
.ui-row .ui-desc { margin-top: 2px; }
.ui-desc + .ui-row { margin-top: 8px; }
/* A setting that only applies once its parent switch is on. */
.ui-row--child { opacity: calc(0.35 + 0.65 * var(--on)); transition: opacity 220ms linear; }
/* Threshold row: label, switch, then the value field. */
.ui-row--field { gap: 12px; padding: 5px 0; }
.ui-row--field .ui-label { flex: 1; }

/* Material 3 switch. --on runs 0 to 1: the thumb grows and travels while the gold track fades in. */
.ui-switch { --on: 0; position: relative; flex: none; width: 52px; height: 32px; border-radius: var(--r-pill); background: var(--raised); box-shadow: inset 0 0 0 2px var(--outline); }
.ui-switch::before { content: ''; position: absolute; inset: 0; border-radius: inherit; background: var(--gold-track); opacity: var(--on); transition: opacity 220ms linear; }
.ui-switch i { position: absolute; left: 4px; top: 4px; width: 24px; height: 24px; border-radius: 50%; background: var(--outline); transform: translateX(calc(var(--on) * 20px)) scale(calc(0.667 + var(--on) * 0.333)); transition: transform 440ms var(--spring); }
.ui-switch i::after { content: ''; position: absolute; inset: 0; border-radius: 50%; background: var(--gold); opacity: var(--on); transition: opacity 220ms linear; }
.ui-switch.is-on { --on: 1; }
/* Scroll-driven state. .flip turns on once --p passes --t. .dip turns off at --t and back on at --u.
   The value snaps at its threshold; the transitions above carry the motion, so a switch lands with the
   spring's momentum instead of tracking the scroll wheel. */
.flip { --on: clamp(0, calc((var(--p) - var(--t)) * 200), 1); }
.dip { --t: 0.3; --u: 0.6; --on: calc(1 - clamp(0, (var(--p) - var(--t)) * 200, 1) + clamp(0, (var(--p) - var(--u)) * 200, 1)); }
.t-a { --t: 0.12; } .t-b { --t: 0.30; } .t-c { --t: 0.48; } .t-d { --t: 0.66; }

/* Outlined field with its label notched into the top border. A plain value is a number field, a bold value with a
   caret is a dropdown. */
.ui-field { position: relative; display: flex; align-items: center; justify-content: space-between; flex: none; width: 150px; height: 48px; padding: 0 6px 0 13px; border: 1px solid var(--outline); border-radius: var(--r-control); background: var(--inset); font-size: 15px; line-height: 1; white-space: nowrap; }
.ui-field-label { position: absolute; left: 9px; top: -8px; padding: 0 5px; background: linear-gradient(var(--raised) 50%, var(--inset) 50%); color: var(--text-secondary); font-size: 12px; line-height: 1.2; }
.ui-field b { font-size: 16px; font-weight: 700; }
.ui-field svg { flex: none; color: var(--text-secondary); }
.ui-panel > .ui-field { width: auto; height: 50px; margin-top: 16px; }
.ui-panel > .ui-field:first-child { margin-top: 6px; }
.ui-field + .ui-desc { margin-top: 6px; }
/* Duration setting: caption, then a number field and its unit side by side. */
.ui-durations { display: grid; gap: 16px 12px; }
@container ui (min-width: 500px) { .ui-durations { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.ui-fields { display: flex; gap: 7px; margin-top: 12px; }
.ui-fields .ui-field { flex: 1; width: auto; min-width: 0; }
.ui-fields .ui-field--select { flex: none; width: 124px; }

/* Thin Material progress bar: gold fill on an outline track. The fixture value is 800 of 4000. */
.ui-bar { height: 4px; overflow: hidden; background: var(--outline); }
.ui-bar i { display: block; height: 100%; background: var(--gold); transform: scaleX(calc(0.2 * clamp(0, var(--p) * 1.6, 1))); transform-origin: left; }
/* Stacked, progress arrives in steps (motion.js); a short ease joins them. */
html:not(.run) .ui-bar i { transition: transform 160ms linear; }

/* Inline warning that the client prints under a switch while it is turned off. */
.ui-warning { margin-top: 2px; color: var(--error); font-size: 13px; line-height: 1.45; opacity: calc(1 - var(--on)); transition: opacity 220ms linear; }

/* Buttons: neutral, gold, success, error and the gold text button. */
.ui-toolbar { display: flex; flex-wrap: wrap; gap: 8px; }
.ui-btn { display: inline-flex; flex: none; align-items: center; gap: 8px; height: 44px; padding: 0 12px; border-radius: var(--r-control); background: var(--raised-high); font-size: 13px; font-weight: 700; line-height: 1; white-space: nowrap; }
.ui-btn--gold { background: var(--gold); color: var(--inset); }
.ui-btn--success { background: var(--success); color: var(--inset); }
.ui-btn--error { background: var(--error); color: var(--inset); }
.ui-btn--text { padding: 0 8px; background: none; color: var(--gold); }
/* The client dims an action that has nothing to act on: Stop All before anything runs, Start All once everything does. */
.ui-rise { opacity: calc(0.42 + 0.58 * var(--on)); transition: opacity 220ms linear; }
.ui-fade { opacity: calc(1 - 0.58 * var(--on)); transition: opacity 220ms linear; }

/* Session card: flat surface, drag handle, checkbox, title with emulator sub-line, plain coloured badge,
   action row, then the status metrics in Plex Mono. A card with .flip starts as the journey passes it. */
.ui-list { display: grid; gap: 8px; }
.ui-card { padding: 14px; border: 1px solid color-mix(in srgb, var(--outline) 80%, transparent); border-radius: var(--r-card); background: var(--raised); }
.ui-card-head { display: flex; align-items: center; gap: 10px; }
.ui-card-head > div { flex: 1; min-width: 0; }
.ui-grip { flex: none; color: var(--text-muted); }
.ui-check { flex: none; width: 18px; height: 18px; margin-right: 4px; border: 2px solid var(--text-secondary); border-radius: 3px; }
.ui-card-title { font-size: 16px; font-weight: 700; line-height: 1.35; }
.ui-badge { font-size: 13px; font-weight: 700; }
.ui-badge--ready { color: var(--success); }
.ui-badge--running { color: var(--gold); }
.ui-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; margin-top: 10px; }
.ui-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px 8px; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--outline-soft); }
@container ui (max-width: 479.98px) { .ui-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.ui-metric-label { color: var(--text-muted); font-size: 12px; font-weight: 500; line-height: 1.35; }
/* Two states stacked in one cell; the pair cross-fades as the profile starts. */
.ui-swap { display: inline-grid; flex: none; justify-items: end; }
.ui-swap > * { grid-area: 1 / 1; transition: opacity 220ms linear; }
.ui-swap > :first-child { opacity: calc(1 - var(--on)); }
.ui-swap > :last-child { opacity: var(--on); }
.ui-metric { justify-items: start; font: 400 13px/1.4 var(--font-mono); font-variant-numeric: tabular-nums; }
.ui-metric > :first-child { color: var(--success); }
.ui-metric > :last-child { color: var(--gold); }

/* Roomier stages show the recreations larger. zoom scales layout as well as paint, so text stays sharp. */
@media (min-width: 1360px) and (min-height: 860px) { .run { --zoom: 1.15; } }
@media (min-width: 1700px) and (min-height: 900px) { .run { --zoom: 1.3; } }
.run .station--split .ui, .run .station--half .ui, .run .ui--pair { zoom: var(--zoom); }

/* Station 08: account safety. The journey rests here for about twice as long as anywhere else.
   Statement, pacing strip, three points with their evidence, and the risk note share one viewport. */

.safety { gap: clamp(12px, 2.2vh, 26px); }
/* The page's one deliberately large statement outside the hero. */
.safety .cap h2 { font-size: clamp(36px, 9.6vw, 60px); line-height: 1.06; letter-spacing: -0.02em; }
.safety .cap-line { max-width: 44em; margin-top: 8px; }

/* Stacked, the sideways pacing is gone, so the station carries its own weight: a full-width inset band,
   the largest heading on the page and the scene's gold key mark on the track. */
html:not(.run) .safety { background: var(--inset); border-block: 1px solid var(--outline-soft); }
html:not(.run) .safety::before { z-index: 2; left: 15px; top: 60px; width: 9px; height: 9px; background: var(--gold); transform: rotate(45deg); }

/* ---------- Pacing strip: irregular marks on a hairline ---------- */
.pace { position: relative; height: 18px; overflow: hidden; }
.pace::before { content: ''; position: absolute; left: 0; right: 0; top: 50%; height: 1px; background: var(--outline); }
.pace i { position: absolute; top: 2px; left: calc(var(--t) * var(--len, 100%)); width: 1px; height: 14px; background: var(--text-secondary); }
/* Thresholds shared by a strip mark and its tap: --t is when it fires, --n is when the next one does. */
.k1 { --t: 0.05; --n: 0.11; } .k2 { --t: 0.11; --n: 0.19; } .k3 { --t: 0.19; --n: 0.24; } .k4 { --t: 0.24; --n: 0.33; }
.k5 { --t: 0.33; --n: 0.40; } .k6 { --t: 0.40; --n: 0.46; } .k7 { --t: 0.46; --n: 0.55; } .k8 { --t: 0.55; --n: 0.61; }
.k9 { --t: 0.61; --n: 0.70; } .k10 { --t: 0.70; --n: 0.78; } .k11 { --t: 0.78; --n: 0.86; } .k12 { --t: 0.86; --n: 2; }
/* Marks still to come when the last tap has landed: the session carries on. */
.k13 { --t: 0.95; } .k14 { --t: 1.03; } .k15 { --t: 1.09; } .k16 { --t: 1.2; } .k17 { --t: 1.26; } .k18 { --t: 1.37; }

/* On the journey the strip crosses the playhead: marks travel left with scroll progress and each one
   reaches the gold line exactly when its tap appears. */
.run .pace { --len: 880px; overflow: visible; margin-left: calc((var(--axis) + var(--pad)) * -1); margin-right: -40px; }
/* Where the strip meets the playhead: the gold line becomes the reading head. */
.run .pace::after { content: ''; position: absolute; left: calc(var(--axis) - 3px); top: 5px; width: 7px; height: 7px; background: var(--gold); transform: rotate(45deg); }
.run .pace i { left: calc(var(--axis) + var(--t) * var(--len)); transform: translate3d(calc(var(--p) * var(--len) * -1), 0, 0); opacity: calc(0.35 + 0.65 * clamp(0, (var(--t) - var(--p)) * 30 + 1, 1)); }

/* ---------- Three points: the claim, its evidence, then what the evidence is ---------- */
/* Each point settles in as a whole (one after another on the journey, each as it enters when stacked),
   so the recreation inside it does not assemble a second time. */
.points { display: grid; gap: 28px; }
.point { --up: clamp(0, calc((var(--p) - var(--a, 0.02)) * 200), 1); display: grid; gap: 10px; align-content: start; min-width: 0; opacity: var(--up); transform: translateY(calc((1 - var(--up)) * 22px)); transition: transform 760ms var(--spring), opacity 240ms linear; }
.run .point:nth-child(2) { --a: 0.05; } .run .point:nth-child(3) { --a: 0.08; }
html:not(.run) .point { --up: 1; }
.point .ui > * { opacity: 1; transform: none; }
.point > p:first-child { font-size: 16px; font-weight: 500; line-height: 1.35; text-wrap: balance; }
.point-fig { min-width: 0; align-self: start; }
.point-fig .ui { width: 100%; max-width: 540px; }
.point-note { margin-top: 0; }
/* The warning keeps its room while hidden. With it gone, the two rows settle to the middle of the panel. */
.ui-panel.dip > .ui-row { --sag: 19px; transform: translateY(calc(var(--on) * var(--sag))); }
@container ui (max-width: 399.98px) { .ui-panel.dip > .ui-row { --sag: 28px; } }
.point--taps svg { width: 100%; max-width: 340px; height: auto; }
.scope { stroke: var(--outline); }
/* A tap lands: it appears at its threshold and the spring settles it to size. */
.tap-dot { --hit: clamp(0, calc((var(--p) - var(--t)) * 200), 1); fill: var(--text); opacity: var(--hit); transform: scale(calc(0.3 + 0.7 * var(--hit))); transform-box: fill-box; transform-origin: center; transition: transform 460ms var(--spring), opacity 140ms linear; }
/* The scene's key mark: a gold ring around the most recent tap only. */
.tap-now { fill: none; stroke: var(--gold); stroke-width: 1.5; opacity: calc(clamp(0, (var(--p) - var(--t)) * 40, 1) * clamp(0, (var(--n) - var(--p)) * 40, 1)); }
.swipe { stroke: var(--text-muted); opacity: clamp(0, calc((var(--p) - var(--t)) * 10), 1); }

@media (min-width: 1000px) {
  /* Claims share one row, evidence the next and labels the last, so the three columns line up. */
  .points { grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.2fr) minmax(0, 1fr); grid-template-rows: auto auto auto; gap: 10px 24px; }
  .point { grid-row: span 3; grid-template-rows: subgrid; }
  .point > p:first-child { align-self: end; }
}

/* ---------- Risk note: part of the scene, never a footnote ---------- */
.risk { display: grid; gap: 6px 32px; padding: 12px 20px; border: 1px solid var(--outline); border-radius: var(--r-card); background: var(--inset); }
html:not(.run) .risk { background: var(--canvas); }
.risk h3 { display: flex; align-items: center; gap: 10px; font-size: 17px; font-weight: 700; line-height: 1.3; }
.risk h3 svg { flex: none; color: var(--warning); }
.risk p { max-width: 62em; color: var(--text-secondary); font-size: 15px; line-height: 1.5; }
.risk .risk-plain { color: var(--text); font-weight: 500; }
.risk-link { display: inline-flex; align-items: center; gap: 6px; min-height: 44px; justify-self: start; color: var(--text); font-size: 15px; font-weight: 500; text-decoration: underline; text-decoration-color: var(--text-muted); text-underline-offset: 5px; white-space: nowrap; }
.risk-link svg { transition: transform 150ms var(--ease-out); }
@media (hover: hover) and (pointer: fine) { .risk-link:hover { text-decoration-color: var(--text); } .risk-link:hover svg { transform: translateX(3px); } }

@media (min-width: 900px) {
  .risk { grid-template-columns: auto minmax(0, 1fr); align-items: start; gap: 8px 48px; }
  .risk h3 { grid-column: 1; grid-row: 1; white-space: nowrap; }
  .risk-link { grid-column: 1; grid-row: 2 / span 2; align-self: start; }
  .risk p { grid-column: 2; }
  .risk p:first-of-type { grid-row: 1 / span 2; }
}

.run .safety { align-content: center; padding-right: 40px; }
/* The statement is set at the hero headline's own size. */
.run .safety .cap h2 { font-size: clamp(32px, min(4.4vw, 7.6vh), 84px); }
/* On the journey the whole station is one screen, so the note closes the scene as a full-width band. */
.run .safety .risk { order: 1; }
.run .safety > * { max-width: calc(1240px * var(--zoom)); }
.run .safety > .pace { max-width: none; }
/* The three pieces of evidence grow with the stage, like every other recreation. */
.run .safety > .points { max-width: 1240px; zoom: var(--zoom); }

/* Short windows: the statement, the three points and the whole risk note still share the screen. */
@media (max-height: 719px) {
  .run .safety { gap: 9px; }
  .run .safety .cap h2 { font-size: clamp(28px, 5.8vh, 40px); }
  .run .safety .cap-line { margin-top: 4px; font-size: 16px; }
  .run .safety .risk { gap: 4px 48px; padding: 8px 18px; }
  .run .safety .points { gap: 6px 24px; }
}
@media (max-height: 639px) { .run .safety { padding-bottom: 20px; } }

/* Rankings: the stop straight after the hero. Two boards, Daily and All-time, of up to three rows each, filled
   in place by behavior/rankings.js. Until real data arrives every row is a neutral disc and dashes.
   This stop carries the one scoped exception to the locked system, approved by the owner for it alone: three
   rank colours, Discord avatars (the only photographs on the site) and names that keep moving. Rank 3
   breathes, rank 2 catches the light, rank 1 burns: three mechanisms, each richer than the one below.
   Everything else is the system's own: flat hairline cards, Plex Mono figures, the shared spring.
   Only transform (and translate) and opacity are animated, so every loop runs on the compositor and no text
   is repainted while it plays. Whatever starts and stops lands on the spring or eases; only the flows that
   never stop (the rare shade and the molten core) keep a steady speed, so their loops have no seam. Nothing
   on the stop rotates: the avatars answer their names with a breath, a glint and a flicker. */

/* Rank colours live on this stop and nowhere else. Each passes 4.5:1 on --canvas and on --raised. */
.rankings { --rank-1: #FF4D4D; --rank-1-ember: #FF8A3D; --rank-1-hot: #FFF4E8; --rank-2: var(--gold); --rank-2-hi: #FFE9A8; --rank-3: #B48CFF; }

/* ---------- Boards ---------- */
/* A figure is its own size container (stations.css), so a container query can never reach this grid itself:
   the two-up switch is a media query. Below 600px the boards stack, Daily first. */
.rk-boards { display: grid; gap: 20px; max-width: 1240px; }
@media (min-width: 600px) { .rk-boards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.rk-board { --av-max: clamp(48px, 5vw, 64px); display: grid; gap: 14px; align-content: start; min-width: 0; padding: 18px; border: 1px solid var(--outline-soft); border-radius: var(--r-card); background: var(--raised); container: board / inline-size; }
.rk-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 14px; }
.rk-head h3 { color: var(--text-secondary); font: 500 15px/1.3 var(--font-sans); }
.rk-window { color: var(--text-muted); font: 400 12px/1.5 var(--font-mono); letter-spacing: 0.01em; }
.rk-list { display: grid; gap: 14px; }
.rk-none { color: var(--text-secondary); font-size: 15px; }
/* On the journey each board settles in as the stop arrives, the way a figure's panels do. journey.js writes
   --p on the board alone; pinning it (and --on) on the children keeps a scroll frame from restyling every
   letter underneath. Stacked, reveal.js lets each board in once as it enters. */
.rk-board > * { --p: 1; --on: 1; }
.run .rk-board { --on: clamp(0, calc((var(--p) - var(--a, 0.015)) * 200), 1); opacity: var(--on); transform: translateY(calc((1 - var(--on)) * 22px)); transition: transform 720ms var(--spring), opacity 220ms linear; }
.run .rk-board + .rk-board { --a: 0.04; }

/* ---------- Rows: the rank, the avatar, then the handle over its two figures ---------- */
/* Every avatar is centred in a column as wide as the largest one, so the three handles share a left edge. */
.rk-row { display: grid; grid-template-columns: auto var(--av-max) minmax(0, 1fr); align-items: center; gap: 0 18px; }
.rk-row--1 { --rank: var(--rank-1); --av: var(--av-max); --size: clamp(26px, 2.5vw, 40px); }
.rk-row--2 { --rank: var(--rank-2); --av: calc(var(--av-max) * 0.8125); --size: clamp(20px, 1.9vw, 28px); }
.rk-row--3 { --rank: var(--rank-3); --av: calc(var(--av-max) * 0.6875); --size: clamp(17px, 1.5vw, 22px); }
.rk-n { color: var(--rank); font: 400 15px/1 var(--font-mono); }
/* The avatar sits on a neutral disc: that is what shows when a player has no avatar or Discord's image fails. */
.rk-av { position: relative; justify-self: center; width: var(--av); height: var(--av); border-radius: 50%; background: var(--raised-high); box-shadow: inset 0 0 0 1px var(--outline); }
.rk-av img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
.rk-av::before, .rk-av::after { content: ''; position: absolute; inset: -5px; border-radius: 50%; }
/* The handle is set in Plex Mono, so every letter is one ch wide and a name can be sized to fit its column
   (Discord allows up to 32 letters; --n is the count, written by rankings.js). Each letter is its own box,
   --i along the name, so it can move and be lit on its own. */
.rk-fit { container-type: inline-size; }
.rk-name { --n: 8; display: block; width: fit-content; color: var(--rank); font: 400 min(var(--size), calc(100cqi / (var(--n) * 0.62)))/1.2 var(--font-mono); white-space: nowrap; }
.rk-l { display: inline-block; }
.rk-stats { display: flex; flex-wrap: wrap; gap: 0 20px; margin-top: 4px; }
.rk-stats div { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 8px; }
.rk-stats dt { color: var(--text-muted); font-size: 13px; line-height: 1.5; }
.rk-stats dd { font: 400 15px/1.5 var(--font-mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* A narrow board (two-up on a small tablet, one-up on a small phone: under 320px across, measured inside its
   padding) trades avatar size and spacing for the handle's room, and sets the figures a size down so each
   still keeps to one line. */
@container board (max-width: 281.98px) {
  .rk-list { --av-max: 40px; }
  .rk-row { column-gap: 10px; }
  .rk-stats div { column-gap: 6px; }
  .rk-stats dt { font-size: 12px; }
  .rk-stats dd { font-size: 13px; }
}

/* ---------- Rank 3, rare: it breathes ---------- */
/* The letters rise through a mask (the site's own word move). Then a soft shade keeps crossing the name, so
   its brightness swells and falls, and a hairline ring opens and closes around the avatar. The shade is one
   veil in the board's own colour sliding over the letters: one moving layer for the name, not one a letter. */
.rk-row--3 [data-rank-name] { position: relative; clip-path: inset(-0.14em -0.1em -0.24em); }
.rk-row--3 .rk-av::after { border: 1px solid var(--rank-3); }
.is-in .rk-row--3 .rk-l { animation: word-in 760ms var(--ease-out) calc(var(--i) * 28ms) backwards; }
/* The veil is twice the name's width and carries its pattern twice, so sliding it one name's width loops. */
.is-in .rk-row--3 [data-rank-name]::after { content: ''; position: absolute; inset: 0 -100% 0 0; background: linear-gradient(90deg, color-mix(in srgb, var(--raised) 20%, transparent), transparent, color-mix(in srgb, var(--raised) 20%, transparent)) 0 0 / 50% 100%; animation: rk-shade 4.2s linear infinite; }
.is-in .rk-row--3 .rk-av::after { animation: rk-breathe 3.6s ease-in-out infinite; }
@keyframes rk-shade { from { transform: translateX(calc(var(--n) * -1ch)); } }
@keyframes rk-breathe { 0%, 100% { opacity: 0.35; transform: scale(1); } 50% { opacity: 1; transform: scale(1.06); } }

/* Ranks 1 and 2 light their letters with copies of the same glyph laid over each one (data-glyph carries the
   character), so a change of colour is a change of opacity. A gradient moving through the letters would
   repaint every one of them on every frame: measured on this stop, that dropped two frames in three. Every
   letter runs the same loop, delayed by its place in the name (--d): that delay makes the light travel. */
.rk-row--1 .rk-l, .rk-row--2 .rk-l { position: relative; }
.rk-row--1 .rk-l::before, .rk-row--1 .rk-l::after, .rk-row--2 .rk-l::after { content: attr(data-glyph); position: absolute; inset: 0; opacity: 0; }

/* ---------- Rank 2, legendary: it catches the light ---------- */
/* The letters lock on one by one: rankings.js cycles them and drops .is-open as each resolves, and the spring
   seats it. Then, every few seconds, one light passes: the avatar's ring brightens and a four-point glint
   flares on it, up and to the left where light would strike it, and a bright band crosses the gold in about
   a second, each letter lifting as it passes. The glint sits on the ring (5px outside the avatar, at 45
   degrees) and scales with it. */
.rk-row--2 .rk-l { --d: calc(var(--i) / var(--n) * 1133ms - 3.4s); transition: opacity 140ms linear, translate 460ms var(--spring); }
.rk-row--2 .rk-l.is-open { opacity: 0.5; translate: 0 0.2em; }
.rk-row--2 .rk-l::after { color: var(--rank-2-hi); }
.rk-row--2 .rk-l.is-open::after { content: none; }
.rk-row--2 .rk-av::before { border: 1px solid var(--rank-2); opacity: 0.55; }
.rk-row--2 .rk-av::after { --g: calc(var(--av) * 0.42); inset: calc(14.6% - 3.5px) auto auto calc(14.6% - 3.5px); width: var(--g); height: var(--g); margin: calc(var(--g) / -2) 0 0 calc(var(--g) / -2); border-radius: 0; background: var(--rank-2-hi); clip-path: polygon(50% 0, 56% 44%, 100% 50%, 56% 56%, 50% 100%, 44% 56%, 0 50%, 44% 44%); opacity: 0; }
.is-in .rk-row--2 .rk-l { animation: rk-lift 3.4s var(--spring) var(--d) infinite; }
.is-in .rk-row--2 .rk-l::after { animation: rk-glint 3.4s ease-in-out var(--d) infinite; }
.is-in .rk-row--2 .rk-av::before { animation: rk-ringlight 3.4s ease-out infinite; }
.is-in .rk-row--2 .rk-av::after { animation: rk-spark 3.4s ease-in-out infinite; }
/* The ring flares at 8% of the cycle and the first letter is brightest at 20%: the avatar catches the light first. */
@keyframes rk-ringlight { 0%, 34%, 100% { opacity: 0.55; } 8% { opacity: 1; } }
@keyframes rk-spark { 0%, 17%, 100% { opacity: 0; transform: scale(0.2); } 8% { opacity: 1; transform: scale(1); } }
@keyframes rk-glint { 0%, 12%, 28%, 100% { opacity: 0; } 20% { opacity: 1; } }
@keyframes rk-lift { 0%, 12%, 30%, 100% { transform: none; } 20% { transform: translateY(-0.09em); } }

/* ---------- Rank 1, mythic: it burns, and it is the one loud element on the stop ---------- */
/* The letters drop in on the spring. Then a white-hot core runs through the red (each letter warms to ember,
   flares and cools again), a hop travels the name, a halo pulses behind it, a corona flickers around the
   avatar and throws off a pulse, and embers rise from it. The corona is a soft band just outside the avatar's
   ring, sized from the avatar, that swells and dims on an uneven beat the way a flame does. */
.rk-row--1 .rk-l { --d: calc(var(--i) / var(--n) * 1.4s - 2.8s); }
.rk-row--1 .rk-l::before { color: var(--rank-1-ember); }
.rk-row--1 .rk-l::after { color: var(--rank-1-hot); }
.rk-row--1 [data-rank-name] { position: relative; isolation: isolate; }
.rk-row--1 [data-rank-name]::before { content: ''; position: absolute; inset: -45% -8%; z-index: -1; background-image: radial-gradient(closest-side, color-mix(in srgb, var(--rank-1) 30%, transparent), transparent); opacity: 0.5; }
.rk-row--1 .rk-av { box-shadow: inset 0 0 0 1px var(--outline), 0 0 0 2px var(--raised), 0 0 0 3px var(--rank-1); }
.rk-row--1 .rk-av::before { inset: -8px; border: 1px solid var(--rank-1-ember); opacity: 0; }
.rk-row--1 .rk-av::after { inset: calc(var(--av) * -0.2); background-image: radial-gradient(closest-side, transparent 72%, color-mix(in srgb, var(--rank-1) 60%, transparent) 82%, transparent); opacity: 0.45; }
.rk-ember { --e: 0; position: absolute; left: calc(20% + var(--e) * 20%); top: 28%; width: 3px; height: 3px; border-radius: 50%; background: var(--rank-1-ember); opacity: 0; }
.rk-ember:nth-of-type(2) { --e: 1; } .rk-ember:nth-of-type(3) { --e: 2; } .rk-ember:nth-of-type(4) { --e: 3; }
/* The drop moves the letter with translate and the hop with transform, and the first hop waits for the landing.
   Two animations on one property of one element are run on the main thread: measured here, sharing transform
   cost a style pass over every moving letter on every display frame. */
.is-in .rk-row--1 .rk-l { animation: rk-hop 4.8s var(--spring) calc(1.8s + var(--i) * 70ms) infinite, rk-drop 720ms var(--spring) calc(var(--i) * 36ms) backwards; }
.is-in .rk-row--1 .rk-l::before { animation: rk-warm 2.8s linear var(--d) infinite; }
.is-in .rk-row--1 .rk-l::after { animation: rk-flare 2.8s linear var(--d) infinite; }
.is-in .rk-row--1 [data-rank-name]::before { animation: rk-halo 2.8s ease-in-out infinite; }
.is-in .rk-row--1 .rk-av::before { animation: rk-pulse 2.4s var(--ease-out) infinite; }
.is-in .rk-row--1 .rk-av::after { animation: rk-flicker 2.3s ease-in-out infinite; }
.is-in .rk-ember { animation: rk-ember 2.6s var(--ease-out) calc(var(--e) * -650ms) infinite; }
@keyframes rk-warm { 0%, 100% { opacity: 0; } 30%, 70% { opacity: 1; } }
@keyframes rk-flare { 0%, 30%, 70%, 100% { opacity: 0; } 50% { opacity: 1; } }
@keyframes rk-hop { 0%, 14%, 100% { transform: none; } 7% { transform: translateY(-0.2em); } }
@keyframes rk-drop { from { translate: 0 -1.4em; opacity: 0; } }
@keyframes rk-halo { 0%, 100% { opacity: 0.3; } 50% { opacity: 0.85; } }
@keyframes rk-pulse { from { transform: scale(1); opacity: 0.8; } to { transform: scale(1.3); opacity: 0; } }
@keyframes rk-flicker { 0%, 100% { opacity: 0.45; transform: scale(1); } 17% { opacity: 0.85; transform: scale(1.05); } 31% { opacity: 0.5; transform: scale(0.98); } 48% { opacity: 1; transform: scale(1.08); } 64% { opacity: 0.6; transform: scale(1.01); } 82% { opacity: 0.9; transform: scale(1.06); } }
@keyframes rk-ember { from { transform: translate3d(0, 0, 0) scale(1); opacity: 0; } 12% { opacity: 1; } to { transform: translate3d(calc((var(--e) - 1.5) * 5px), -46px, 0) scale(0.4); opacity: 0; } }

/* Short windows: the caption and both boards still share one stage. */
@media (max-height: 719px) {
  .run .rk-board { --av-max: 52px; gap: 10px; padding: 14px; }
  .run .rk-list { gap: 12px; }
}

/* ---------- Off screen, every loop is held ---------- */
/* Declared last and as !important: each animation shorthand above also sets the play state back to running,
   and several of those selectors are more specific than this one. */
.rankings:not(.is-live) .rk-row *, .rankings:not(.is-live) .rk-row *::before, .rankings:not(.is-live) .rk-row *::after { animation-play-state: paused !important; }

/* Recorded community progress: the live data chapter. In normal flow it is two panels, one above the other,
   and each village and graph plays as it scrolls into view.
   Where the window allows (html.data-run) the stage holds, sticky inside its tall wrapper, and data-scroll.js
   plays both beats from one progress value: the totals rise in, the stage pans sideways as the run does, then
   scroll plays the last 24 hours. */
.data { background: var(--inset); border-top: 1px solid var(--outline-soft); }
.data-stage { display: grid; gap: 28px; align-content: start; padding: 72px 16px 40px 44px; }
.data-head h2 { font: 500 clamp(27px, 2.7vw, 40px)/1.1 var(--font-sans); letter-spacing: -0.015em; }
.data-status { margin-top: 10px; color: var(--text-muted); font: 400 12px/1.5 var(--font-mono); letter-spacing: 0.01em; }
.data-lens, .data-panel { min-width: 0; }
.data-world { display: grid; gap: 64px; }
.village h3, .hourly-head h3, .chart h4 { color: var(--text-secondary); font: 500 15px/1.3 var(--font-sans); }

/* ---------- Beat 1: consolidated totals ---------- */
.totals { display: grid; gap: 40px 48px; align-content: start; }
.loot { display: flex; flex-wrap: wrap; gap: 16px 40px; margin-top: 14px; }
.loot dt { color: var(--text-muted); font-size: 13px; line-height: 1.3; }
.loot dd { margin-top: 4px; font: 400 clamp(26px, 2.6vw, 40px)/1 var(--font-mono); letter-spacing: -0.03em; font-variant-numeric: tabular-nums; white-space: nowrap; }
.work { margin-top: 18px; border-top: 1px solid var(--outline); }
.work > div { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; padding: 7px 0; border-bottom: 1px solid var(--outline-soft); }
.work dt { color: var(--text-secondary); font-size: 14px; line-height: 1.35; }
.work dd { font: 400 15px/1.3 var(--font-mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
.village--clan .work { margin-top: 14px; }
/* Rows arrive in reading order as --p passes their threshold and the spring settles each one into place.
   The default --p is 1, so nothing depends on script. */
.rise { --on: clamp(0, calc((var(--p) - var(--t, 0)) * 200), 1); opacity: var(--on); transform: translateY(calc((1 - var(--on)) * 16px)); transition: transform 560ms var(--spring), opacity 260ms linear; }
/* On the held stage the three villages share one progress value, so each starts a little later than the last.
   In normal flow every row arrives on its own as it enters the viewport (reveal.js). */
html:not(.data-run) .rise { --on: 1; }
.data-run .village:nth-child(2) { --t0: 0.2; }
.data-run .village:nth-child(3) { --t0: 0.4; }
.loot > :nth-child(1) { --t: calc(var(--t0, 0) + 0.02); }
.loot > :nth-child(2) { --t: calc(var(--t0, 0) + 0.05); }
.loot > :nth-child(3) { --t: calc(var(--t0, 0) + 0.08); }
.work > :nth-child(1) { --t: calc(var(--t0, 0) + 0.12); }
.work > :nth-child(2) { --t: calc(var(--t0, 0) + 0.14); }
.work > :nth-child(3) { --t: calc(var(--t0, 0) + 0.16); }
.work > :nth-child(4) { --t: calc(var(--t0, 0) + 0.18); }
.work > :nth-child(5) { --t: calc(var(--t0, 0) + 0.2); }
.work > :nth-child(6) { --t: calc(var(--t0, 0) + 0.22); }
.work > :nth-child(7) { --t: calc(var(--t0, 0) + 0.24); }

/* ---------- Beat 2: the last 24 hours ---------- */
.hourly { display: grid; gap: 22px 48px; align-content: start; }
.hourly-head { grid-column: 1 / -1; }
.chart { display: grid; gap: 10px; align-content: start; min-width: 0; }
.currencies { display: flex; flex-wrap: wrap; gap: 8px; }
.currencies button { display: grid; gap: 3px; min-width: 104px; min-height: 48px; padding: 6px 12px; border: 1px solid var(--outline); border-radius: var(--r-control); background: transparent; text-align: left; cursor: pointer; }
.currencies button[aria-pressed="true"] { border-color: var(--gold); background: var(--raised); }
.currencies button:disabled { cursor: default; }
.currencies span { color: var(--text-secondary); font-size: 12px; line-height: 1.2; }
.currencies strong { font: 400 16px/1.2 var(--font-mono); font-variant-numeric: tabular-nums; }
@media (hover: hover) and (pointer: fine) { .currencies button:enabled:hover { background: var(--raised); } }
.chart-peak, .chart-readout, .plot-axis { font-family: var(--font-mono); font-weight: 400; }
.chart-peak { color: var(--text-muted); font-size: 12px; line-height: 1.4; }
.chart-readout { min-height: 2.8em; color: var(--text-secondary); font-size: 12px; line-height: 1.4; }
/* The plot is the hour control: hover, drag sideways or use the arrow keys. Vertical swipes still scroll the page. */
.plot { position: relative; aspect-ratio: 640 / 208; overflow: hidden; touch-action: pan-y; border: 1px solid var(--outline-soft); border-radius: var(--r-control); background: var(--raised); }
.plot svg { width: 100%; height: 100%; cursor: crosshair; }
.plot svg:focus-visible { outline-offset: -3px; }
.plot-grid path { fill: none; stroke: var(--outline-soft); stroke-width: 1; vector-effect: non-scaling-stroke; }
.plot-area { fill: var(--gold); fill-opacity: 0.1; }
.plot-line { fill: none; stroke: var(--gold); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }
/* The hour marker glides to the chosen hour and its dot lands with the spring. */
.plot-cursor { fill: none; stroke: var(--text-muted); stroke-width: 1; stroke-dasharray: 3 3; vector-effect: non-scaling-stroke; transition: transform 320ms var(--ease-out); }
.plot-dot { fill: var(--gold); }
.plot-now { fill: var(--gold); stroke: var(--raised); stroke-width: 2; transition: transform 460ms var(--spring); }
/* The wipe: a cover the colour of the plot retreats to the right as scroll plays the hours. */
.plot-cover { position: absolute; inset: 0; background: var(--raised); transform-origin: right; transform: scaleX(calc(1 - var(--sweep, 1))); pointer-events: none; }
/* In normal flow the sweep arrives in steps (motion.js); a short ease joins them. */
html:not(.data-run) .plot-cover { transition: transform 160ms linear; }
/* Choosing another currency redraws its line from the left. */
.plot.is-redrawn .plot-cover { animation: plot-redraw 700ms var(--ease-out); }
@keyframes plot-redraw { from { transform: none; } to { transform: translateX(101%); } }
.plot-empty { position: absolute; inset: 0; display: grid; place-items: center; padding: 12px; color: var(--text-muted); font-size: 13px; text-align: center; }
/* The axis labels sit under the plotted span of the graph (chartBounds in loot-history.mjs). */
.plot-axis { display: flex; justify-content: space-between; padding: 0 2.5% 0 8.1%; color: var(--text-muted); font-size: 11px; line-height: 1; }

.data-notes { display: grid; gap: 8px; max-width: 76em; padding: 0 16px 72px 44px; color: var(--text-muted); font-size: 13px; line-height: 1.5; }
.data-notes .link { justify-self: start; font-size: 14px; }

@media (min-width: 900px) {
  .data-stage { padding: 96px 40px 48px 84px; }
  .data-notes { padding: 0 40px 96px 84px; }
  .totals { grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr) minmax(0, 0.9fr); }
  .hourly { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* On the journey everything after the track lines up on the playhead axis. */
.run .data-stage, .run .data-notes { padding-left: calc(var(--axis) + var(--pad)); }

/* ---------- Held stage: two screens of scroll play it ---------- */
.data-run .data-pin { height: calc(300dvh - var(--bar)); }
.data-run .data-stage { position: sticky; top: var(--bar); height: calc(100dvh - var(--bar)); grid-template-rows: auto minmax(0, 1fr); padding-block: clamp(28px, 5vh, 56px) 28px; overflow: clip; }
/* The lens is 16px wider than the copy on each side, so panels dissolve at its edges instead of being cut. */
.data-run .data-lens { margin-inline: -16px; overflow: clip; -webkit-mask-image: linear-gradient(to right, transparent, var(--inset) 16px, var(--inset) calc(100% - 16px), transparent); mask-image: linear-gradient(to right, transparent, var(--inset) 16px, var(--inset) calc(100% - 16px), transparent); }
.data-run .data-world { display: flex; gap: 0; width: 200%; height: 100%; transform: translate3d(calc(var(--pan, 0) * -50%), 0, 0); will-change: transform; }
.data-run .data-panel { flex: 0 0 50%; padding: 0 56px 0 16px; }
/* The pinned stage has room to spare on a tall window: the ledger opens up with it. */
.data-run .loot dd { font-size: clamp(26px, min(3.1vw, 5vh), 48px); }
.data-run .work > div { padding-block: clamp(7px, 1.1vh, 10px); }

/* Five-star reviews: one looping rail on Home, a wall on Community, and Community's progress ticker. */
.proof { position: relative; z-index: 1; padding-block: clamp(72px, 13vh, 144px); background: var(--canvas); border-top: 1px solid var(--outline-soft); }

.rating-section { width: 100%; max-width: 1240px; min-width: 0; }
.rating-heading { display: grid; gap: 12px; }
.rating-heading h2 { font: 500 clamp(26px, 2.8vw, 40px)/1.1 var(--font-sans); letter-spacing: -0.016em; }
.rating-heading p { max-width: 44em; color: var(--text-secondary); font-size: 15px; }
[data-reviews="rail"] .rating-heading { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 24px; }
:is(.rating-status, .rating-note) { margin-top: 16px; color: var(--text-muted); font-size: 13px; }
.rating-note { font-size: 12px; }

/* A column whose attribution sits on the bottom edge, so cards of one row line up whatever their text length. */
.rating-card { display: flex; flex-direction: column; gap: 14px; min-width: 0; margin: 0; padding: 20px; border: 1px solid var(--outline-soft); border-radius: var(--r-card); background: var(--raised); }
.rating-card blockquote { margin: 0; }
:is(.home-page, .inner-page) .rating-card blockquote p { color: var(--text); font-size: 16px; white-space: pre-wrap; overflow-wrap: anywhere; }
.rating-stars { color: var(--gold); font-size: 15px; line-height: 1; letter-spacing: 2px; }
.rating-author { display: flex; align-items: center; gap: 12px; margin: auto 0 0; color: var(--text-muted); font: 400 13px/1.4 var(--font-sans); letter-spacing: 0; }
.rating-author strong { color: var(--text-secondary); font-weight: 500; }
.rating-avatar { position: relative; display: grid; place-items: center; flex: 0 0 40px; width: 40px; height: 40px; overflow: hidden; border-radius: 50%; background: var(--raised-high); color: var(--gold); }
.rating-avatar img { position: absolute; inset: 0; object-fit: cover; }
.rating-attribution { display: grid; min-width: 0; gap: 3px; overflow-wrap: anywhere; }

/* ---------- The wall (Community) ---------- */
.rating-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-top: 28px; }
.rating-grid:empty { display: none; }
/* Cards settle in as they arrive, the first twelve and each later dozen alike. */
.rating-grid .rating-card { animation: settle-in 720ms var(--spring) backwards, fade-in 380ms linear backwards; }
.rating-grid .rating-card:nth-child(3n + 2) { animation-delay: 70ms; }
.rating-grid .rating-card:nth-child(3n) { animation-delay: 140ms; }
.rating-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 20px; }
.rating-actions .rating-status { flex: 1 1 280px; }
.rating-toggle { align-self: start; min-height: 44px; padding: 0; border: 0; background: transparent; color: var(--text); font-size: 14px; text-decoration: underline; text-decoration-color: var(--outline); text-underline-offset: 5px; cursor: pointer; }
.rating-toggle:hover { text-decoration-color: var(--gold); }
.rating-toggle:disabled { opacity: 0.55; cursor: wait; }
@media (max-width: 899px) { .rating-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 599px) { .rating-grid { grid-template-columns: minmax(0, 1fr); } }

/* ---------- Looping rows: the review rail (Home) and the progress ticker (Community) ---------- */
/* By owner decision these two rows loop, the second exception to the scroll-only rule after the Rankings names.
   As built, a row is one to scroll or swipe sideways, which is what a touch screen and a page without script
   keep. With a fine pointer behavior/ratings.js doubles the set and adds .is-loop: the track then runs on the
   compositor, one transform animation and nothing else, and is held while the row is off screen (.is-live). */
.marquee { --fade: clamp(32px, 7vw, 88px); --edge: black; margin-top: 28px; overflow-x: auto; overscroll-behavior-x: contain; scroll-snap-type: x proximity; scrollbar-width: none; -webkit-mask-image: linear-gradient(to right, var(--edge), black var(--fade), black calc(100% - var(--fade)), transparent); mask-image: linear-gradient(to right, var(--edge), black var(--fade), black calc(100% - var(--fade)), transparent); }
.marquee::-webkit-scrollbar { display: none; }
/* The mask would hide a ring drawn outside the box. */
.marquee:focus-visible { outline-offset: -2px; }
/* The scroller's last item stops short of the faded edge; the loop needs no such room. */
.marquee-track { --gap: 16px; display: flex; gap: var(--gap); width: max-content; margin: 0; padding: 0 var(--fade) 0 0; list-style: none; }
.marquee-track > * { scroll-snap-align: start; }
@keyframes marquee { to { transform: translate3d(calc(-50% - var(--gap) / 2), 0, 0); } }
@media (hover: hover) and (pointer: fine) {
  .marquee.is-loop { --edge: transparent; overflow: hidden; }
  .marquee.is-loop .marquee-track { padding: 0; animation: marquee var(--marquee-s, 120s) linear infinite; }
  .marquee.is-loop:not(.is-live) .marquee-track { animation-play-state: paused; }
  .progress-ticker.is-loop .marquee-track { animation-direction: reverse; }
}

.review-rail .rating-card { flex: 0 0 auto; width: min(340px, 78vw); }
.review-rail .rating-card blockquote p { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 4; overflow: hidden; white-space: normal; }

.progress-ticker { --fade: clamp(24px, 5vw, 64px); }
.progress-ticker .marquee-track { --gap: 12px; }
.progress-chip { display: flex; align-items: baseline; gap: 10px; padding: 10px 16px; border: 1px solid var(--outline-soft); border-radius: var(--r-pill); background: var(--raised); color: var(--text-secondary); font-size: 15px; white-space: nowrap; }
.progress-chip strong { color: var(--text); font-weight: 500; }
.progress-chip cite { color: var(--text-muted); font-size: 13px; font-style: normal; }

/* Inner pages: one column, two-tone headings, hairline sections and the closing action.
   Everything here is scoped to .inner-page or to its own block, so the home journey is untouched. */
.wrap { width: min(100% - 2 * var(--gutter), 1240px); margin-inline: auto; }
.breadcrumbs { display: flex; align-items: center; gap: 10px; min-height: 44px; margin-top: 12px; color: var(--text-muted); font: 400 12px/1 var(--font-mono); letter-spacing: 0.02em; }
.breadcrumbs a { display: inline-flex; align-items: center; min-height: 44px; color: var(--text-secondary); text-decoration: none; }

/* Headings: sentence case in Plex Sans. The second clause is muted, so claim and support share one line. */
h1 em, h2 em { color: var(--text-muted); }
.inner-page main h1, .error-page h1 { max-width: 16ch; font: 500 clamp(36px, 5vw, 68px)/1.04 var(--font-sans); letter-spacing: -0.024em; text-wrap: balance; }
/* The page's search phrase sits above its headline as a quiet label. */
.inner-page h1 small { display: block; margin-bottom: 16px; color: var(--text-muted); font: 400 13px/1.2 var(--font-mono); letter-spacing: 0.04em; }
.inner-page main h2 { font: 500 clamp(26px, 2.8vw, 40px)/1.1 var(--font-sans); letter-spacing: -0.016em; text-wrap: balance; }
.inner-page main h3 { font: 500 18px/1.3 var(--font-sans); }
.inner-page main p, .error-page p { color: var(--text-secondary); text-wrap: pretty; }
.inner-page main a:not(.btn, .link, .plan-action) { color: var(--text); text-underline-offset: 4px; text-decoration-color: var(--text-muted); }

.page-intro { display: grid; gap: 20px; padding-block: clamp(28px, 6vh, 64px) clamp(40px, 7vh, 72px); }
.page-intro p { max-width: 38em; font-size: 18px; }
.page-intro .reading-date { font-size: 12px; }
.inner-page main > section + section, .inner-page main > .reading-layout { padding-block: clamp(40px, 7vh, 80px); border-top: 1px solid var(--outline-soft); }
.page-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 24px; margin-top: 28px; }
.page-intro .page-actions { margin-top: 8px; }

/* Closing action, shared with the home page. */
.page-close { position: relative; z-index: 1; padding-block: clamp(72px, 13vh, 144px); background: var(--canvas); border-top: 1px solid var(--outline-soft); }
.page-close h2 { max-width: 20ch; font: 500 clamp(30px, 3.2vw, 46px)/1.1 var(--font-sans); letter-spacing: -0.02em; text-wrap: balance; }
.page-close > p { max-width: 36em; margin-top: 16px; color: var(--text-secondary); }
.page-close .hero-act { margin-top: 28px; gap: 8px 24px; }
/* On the home page everything after the track lines up with the station copy. */
.home-page .page-close, .home-page .proof { padding-inline: 44px 16px; }
@media (min-width: 900px) { .home-page .page-close, .home-page .proof { padding-inline: 84px 40px; } }
.run .page-close, .run .proof { padding-left: calc(var(--axis) + var(--pad)); }

/* Three short topics in a row, divided by hairlines (community, payment methods). */
.topics { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 28px 40px; margin-top: 8px; }
.topics article { display: grid; gap: 10px; align-content: start; padding-top: 20px; border-top: 1px solid var(--outline); }
.inner-page .topics h2 { font: 500 18px/1.3 var(--font-sans); letter-spacing: 0; }
.plan-inclusions > p { max-width: 40em; margin-top: 14px; }
.plan-inclusions .topics { margin-top: 32px; }

/* ---------- Pricing ---------- */
.pricing-source { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 24px; }
.pricing-source p, .gallery-note, .changelog-status p, .reading-date { color: var(--text-muted); font: 400 12px/1.5 var(--font-mono); letter-spacing: 0.01em; }
/* Longer qualifications stay in the reading face. */
.inner-page .pricing-help, .inner-page .release-note, .inner-page .changelog-footnote { color: var(--text-muted); font-size: 13px; line-height: 1.5; }
.price-refresh, .changelog-refresh { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 0 14px; border: 1px solid var(--outline); border-radius: var(--r-control); background: transparent; font-size: 14px; font-weight: 500; cursor: pointer; }
/* The refresh button and the longer live status arrive after load. Their room is kept from the start, so the
   plans below never move when live prices come in. */
.price-refresh[hidden] { display: inline-flex !important; visibility: hidden; }
@media (max-width: 599px) { .pricing-source p { flex-basis: 100%; min-height: 3em; } }
.pricing-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 16px; margin-top: 20px; }
.plan { display: grid; gap: 6px; align-content: start; padding: 24px; border: 1px solid var(--outline-soft); border-radius: var(--r-card); background: var(--raised); }
.plan-free { border-color: var(--gold-track); }
.inner-page .plan h2 { font: 500 17px/1.3 var(--font-sans); letter-spacing: 0; }
.inner-page .plan-price { margin-top: 10px; color: var(--text); font: 400 40px/1 var(--font-mono); letter-spacing: -0.04em; }
.plan-price span { margin-left: 6px; color: var(--text-muted); font-size: 13px; letter-spacing: 0; }
.plan-term, .plan-cadence { font-size: 14px; }
.plan-action { display: inline-flex; align-items: center; gap: 6px; min-height: 44px; margin-top: 8px; color: var(--gold); font-size: 15px; font-weight: 500; text-decoration: none; }
.inner-page .pricing-help { max-width: 70em; margin-top: 20px; }
.pricing-unavailable { grid-column: 1 / -1; padding: 24px; border: 1px dashed var(--outline); border-radius: var(--r-card); }

/* ---------- Download ---------- */
.release-card { display: grid; gap: 14px; justify-items: start; max-width: 760px; padding: 28px; border: 1px solid var(--outline-soft); border-radius: var(--r-card); background: var(--raised); }
.release-summary { display: flex; align-items: center; gap: 18px; }
.inner-page .release-summary h2 { font: 500 22px/1.2 var(--font-sans); letter-spacing: -0.01em; }
.release-summary p { margin-top: 4px; font-size: 15px; }
.release-links { display: flex; flex-wrap: wrap; gap: 0 24px; }
.checksum { width: 100%; border-top: 1px solid var(--outline-soft); }
.checksum code { display: block; padding-bottom: 12px; color: var(--text-secondary); font: 400 13px/1.5 var(--font-mono); overflow-wrap: anywhere; }
.steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 28px 40px; margin-top: 32px; counter-reset: step; }
.steps li { display: grid; gap: 10px; align-content: start; padding-top: 20px; border-top: 1px solid var(--outline); counter-increment: step; }
.steps li::before { content: counter(step, decimal-leading-zero); color: var(--text-muted); font: 400 13px/1 var(--font-mono); letter-spacing: 0.06em; }

/* ---------- Not found ---------- */
.error-page { display: grid; min-height: 100svh; place-items: center; padding: 40px var(--gutter); }
.error-page main { display: grid; gap: 20px; width: min(100%, 640px); }
.error-page h1 { margin-top: 40px; }
@media (hover: hover) and (pointer: fine) { .price-refresh:hover, .changelog-refresh:hover { background: var(--hover-wash); } .breadcrumbs a:hover { color: var(--text); } }

/* Features page: the genuine client renders, switched by tabs. */
.feature-intro > div { display: grid; gap: 12px; justify-items: start; }
.gallery-tools { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px 24px; }
.gallery-tools > span { color: var(--text-muted); font: 400 12px/1.4 var(--font-mono); letter-spacing: 0.01em; }
.gallery-tabs { display: flex; flex-wrap: wrap; gap: 8px; }
.gallery-tabs button { min-height: 44px; padding: 0 16px; border: 1px solid var(--outline); border-radius: var(--r-control); background: transparent; color: var(--text-secondary); font-size: 14px; font-weight: 500; cursor: pointer; }
.gallery-tabs button[aria-selected="true"] { border-color: var(--gold); background: var(--raised); color: var(--text); }
.client-frame { margin-top: 16px; overflow: hidden; border: 1px solid var(--outline-soft); border-radius: var(--r-card); background: var(--raised); }
.client-panel img { width: 100%; height: auto; }
.client-panel figcaption { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 4px 24px; margin: 0; padding: 10px 20px; border-top: 1px solid var(--outline-soft); }
.client-panel figcaption a { display: inline-flex; align-items: center; gap: 6px; min-height: 44px; }
.gallery-note { margin-top: 12px; }
@media (hover: hover) and (pointer: fine) { .gallery-tabs button:hover { color: var(--text); } }

/* Features page: the full capability catalogue. Each category is a closed row until it is asked for,
   so the page stays quiet while every detail remains on it. */
.capabilities { display: grid; gap: 32px 64px; }
.capabilities-intro { display: grid; gap: 14px; align-content: start; }
.feature-jumps { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.inner-page .feature-jumps a { display: inline-flex; align-items: center; min-height: 36px; padding: 0 12px; border: 1px solid var(--outline-soft); border-radius: var(--r-pill); color: var(--text-secondary); font-size: 13px; text-decoration: none; }
.capability-index details { border-top: 1px solid var(--outline); }
.capability-index details:last-child { border-bottom: 1px solid var(--outline); }
.capability-index summary > div { display: grid; gap: 4px; padding-block: 16px; }
.inner-page .capability-index summary h3 { font-size: 19px; }
.inner-page .capability-index summary p { color: var(--text-muted); font-size: 14px; }
.capability-index ul { display: grid; gap: 10px; padding: 0 0 24px 1.1em; list-style: disc; color: var(--text-secondary); font-size: 16px; }
.capability-index ul li::marker { color: var(--text-muted); }
.capability-index details > p { max-width: 60em; padding-bottom: 16px; font-size: 15px; }
.roster { display: grid; gap: 8px; padding-bottom: 24px; }
.inner-page .roster h3 { margin-top: 12px; color: var(--text-muted); font: 400 12px/1.4 var(--font-mono); letter-spacing: 0.04em; }
.roster p { font-size: 15px; }
.availability-note { margin-top: 8px; color: var(--text-muted); font-size: 13px; }
@media (min-width: 1000px) {
  .capabilities { grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.6fr); }
  .capabilities-intro { position: sticky; top: calc(var(--bar) + 32px); }
}
@media (hover: hover) and (pointer: fine) { .inner-page .feature-jumps a:hover { color: var(--text); border-color: var(--outline); } }

/* Disclosures: every details element on the site opens the same way. */
summary { display: flex; align-items: center; justify-content: space-between; gap: 20px; min-height: 56px; cursor: pointer; list-style: none; }
summary::-webkit-details-marker { display: none; }
/* The caret is drawn from two borders and turns over when the row opens. */
summary::after { content: ''; flex: none; width: 9px; height: 9px; margin-right: 6px; border-right: 1.5px solid var(--text-secondary); border-bottom: 1.5px solid var(--text-secondary); transform: translateY(-3px) rotate(45deg); transition: transform 240ms var(--ease-out); }
details[open] > summary::after { transform: translateY(2px) rotate(225deg); }
@supports (interpolate-size: allow-keywords) {
  details { interpolate-size: allow-keywords; }
  details::details-content { block-size: 0; overflow: clip; opacity: 0; transition: block-size 320ms var(--ease-out), opacity 240ms linear, content-visibility 320ms allow-discrete; }
  details[open]::details-content { block-size: auto; opacity: 1; }
}

/* Watch page: the code-authored client tour and its chapter list. */
.watch-player video { width: 100%; height: auto; border: 1px solid var(--outline-soft); border-radius: var(--r-card); background: var(--inset); }
.watch-player p { margin-top: 14px; font-size: 15px; }
.watch-guide { display: grid; gap: 32px 64px; }
.watch-guide > div { display: grid; gap: 14px; align-content: start; justify-items: start; }
.video-chapters li { display: grid; gap: 4px; padding-block: 12px; border-top: 1px solid var(--outline-soft); }
.inner-page .video-chapters a { display: flex; align-items: baseline; gap: 16px; min-height: 44px; padding-top: 8px; color: var(--text); font-size: 17px; font-weight: 500; text-decoration: none; }
.video-chapters time { flex: none; width: 3.2em; color: var(--text-muted); font: 400 13px/1 var(--font-mono); }
.video-chapters p { padding-left: calc(3.2em + 16px); font-size: 15px; }
@media (min-width: 1000px) {
  .watch-guide { grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.6fr); }
  .watch-guide > div { position: sticky; top: calc(var(--bar) + 32px); }
}

/* Reading pages (setup guide, privacy policy, terms of service): a contents rail beside one readable column. */
.reading-layout { display: grid; gap: 32px 64px; }
.reading-index { display: grid; align-content: start; }
.inner-page .reading-index a { display: flex; align-items: center; min-height: 40px; color: var(--text-secondary); font-size: 14px; text-decoration: none; }
.reading-content { min-width: 0; max-width: 46em; }
.reading-content section { display: grid; gap: 14px; padding-block: 32px; border-top: 1px solid var(--outline-soft); scroll-margin-top: calc(var(--bar) + 16px); }
.reading-content section:first-child { padding-top: 0; border-top: 0; }
.inner-page .reading-content h2 { font-size: clamp(22px, 2.2vw, 28px); }
.reading-content ul, .reading-content ol { display: grid; gap: 8px; padding-left: 1.3em; color: var(--text-secondary); }
.reading-content ul { list-style: disc; }
.reading-content ol { list-style: decimal; }
.reading-content li::marker { color: var(--text-muted); }
.reading-content li ul, .reading-content li ol { margin-top: 8px; }
.reading-content strong { color: var(--text); font-weight: 500; }
.reading-content code { color: var(--text); font: 400 0.9em var(--font-mono); }
.guide-visual { display: grid; gap: 10px; justify-items: start; }
.guide-visual img { max-width: 100%; height: auto; border: 1px solid var(--outline-soft); border-radius: var(--r-card); }
.guide-visual figcaption { display: grid; gap: 2px; margin: 0; }
.guide-visual figcaption strong { font-family: var(--font-sans); font-size: 14px; }
.guide-sources, .guide-tutorials { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.inner-page .guide-sources a, .inner-page .guide-tutorials a { display: inline-flex; align-items: center; gap: 10px; min-height: 44px; padding: 6px 14px; border: 1px solid var(--outline-soft); border-radius: var(--r-control); color: var(--text-secondary); font-size: 14px; text-decoration: none; }
.guide-tutorials h3 { flex-basis: 100%; }
.guide-tutorials a span { display: grid; }
.guide-tutorials small { color: var(--text-muted); font-size: 12px; }
@media (min-width: 1000px) {
  .reading-layout { grid-template-columns: 250px minmax(0, 1fr); }
  .reading-index { position: sticky; top: calc(var(--bar) + 24px); max-height: calc(100dvh - var(--bar) - 48px); overflow-y: auto; }
}
@media (hover: hover) and (pointer: fine) { .inner-page .reading-index a:hover, .inner-page .guide-sources a:hover, .inner-page .guide-tutorials a:hover { color: var(--text); } }

/* About page: the statement beside the facts, then the story and three principles. */
.facts { display: grid; align-content: start; border-top: 1px solid var(--outline); }
.facts > div { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 24px; padding-block: 12px; border-bottom: 1px solid var(--outline-soft); }
.facts dt { color: var(--text-muted); font: 400 13px/1.4 var(--font-mono); letter-spacing: 0.02em; }
.facts dd { color: var(--text); font-size: 16px; }
.about-intro > div { display: grid; gap: 20px; align-content: start; }
.about-story { display: grid; gap: 24px 64px; }
.about-story > div { display: grid; gap: 16px; max-width: 42em; font-size: 18px; }
.about-principles { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 28px 40px; }
.about-principles > div { display: grid; gap: 10px; align-content: start; justify-items: start; padding-top: 20px; border-top: 1px solid var(--outline); }
@media (min-width: 1000px) {
  .about-intro { grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr); gap: 64px; align-items: end; }
  .about-story { grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.6fr); }
}

/* Changelog: a version rail beside the release notes. */
.changelog-layout { display: grid; gap: 32px 64px; }
.changelog-index { display: grid; gap: 10px; align-content: start; justify-items: start; }
.inner-page .changelog-index h2 { font: 500 18px/1.3 var(--font-sans); letter-spacing: 0; }
.changelog-index p { font-size: 14px; }
#version-index { display: flex; flex-wrap: wrap; gap: 0 18px; }
.inner-page #version-index a { display: inline-flex; align-items: center; gap: 6px; min-height: 40px; color: var(--text-secondary); font: 400 13px/1 var(--font-mono); text-decoration: none; }
#version-index .icon { display: none; }
.changelog-feed { min-width: 0; }
.changelog-status { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 24px; padding-bottom: 16px; }
.changelog-entry { padding-block: 8px; border-top: 1px solid var(--outline-soft); scroll-margin-top: calc(var(--bar) + 16px); }
.changelog-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px; padding-top: 16px; color: var(--text-muted); font: 400 13px/1.2 var(--font-mono); }
.changelog-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--outline); }
.changelog-latest .changelog-dot { background: var(--gold); }
.changelog-version { color: var(--text); }
.release-label { padding: 4px 8px; border: 1px solid var(--outline); border-radius: var(--r-pill); color: var(--text-secondary); font-size: 11px; }
.inner-page .changelog-entry h2 { font: 500 20px/1.3 var(--font-sans); letter-spacing: -0.005em; }
.changelog-notes { display: grid; gap: 12px; max-width: 46em; padding-bottom: 20px; color: var(--text-secondary); font-size: 16px; }
.changelog-notes ul, .changelog-notes ol { display: grid; gap: 8px; padding-left: 1.2em; list-style: disc; }
.changelog-notes li::marker { color: var(--text-muted); }
.changelog-notes strong { color: var(--text); font-weight: 500; }
.changelog-notes code { color: var(--text); font: 400 0.9em var(--font-mono); }
.inner-page .changelog-notes h3 { margin-top: 8px; font-size: 16px; }
.changelog-footnote { margin-top: 24px; }
.changelog-empty { display: grid; gap: 10px; padding: 32px; border: 1px dashed var(--outline); border-radius: var(--r-card); }
@media (min-width: 1000px) {
  .changelog-layout { grid-template-columns: 250px minmax(0, 1fr); }
  .changelog-index { position: sticky; top: calc(var(--bar) + 24px); }
  #version-index { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (hover: hover) and (pointer: fine) { .inner-page #version-index a:hover { color: var(--text); } }

/* Motion: one vocabulary for every page.
   Words rise through a mask. Blocks rise and settle on a spring. Lines draw from their origin. Mono labels wipe
   on in steps, like type. On the home journey the same moves are tied to scroll progress (--p, in stations.css
   and client.css); everywhere else they play once: on load for the first screen, and as each block enters the
   viewport below it (behavior/reveal.js).
   Every rule animates from a hidden state to the element's own finished style, so without script, or if an
   animation never runs, the page is complete. Motion is not gated on the operating-system preference: that is
   the site's standing decision (website/README.md). */

/* ---------- Kinetic words: markup comes from src/kinetic.mjs ---------- */
/* The mask is a clip, not overflow, so the word keeps its baseline; the insets leave room for descenders. */
.w { display: inline-block; clip-path: inset(-0.14em -0.1em -0.24em); }
.w > span { display: inline-block; transform: translateY(calc((1 - var(--in, 1)) * 128%)); transition: transform 760ms var(--ease-out); transition-delay: calc(var(--kd, 0ms) + var(--wi, 0) * 42ms); }
.w2 { --wi: 1; } .w3 { --wi: 2; } .w4 { --wi: 3; } .w5 { --wi: 4; } .w6 { --wi: 5; } .w7 { --wi: 6; }
.w8 { --wi: 7; } .w9 { --wi: 8; } .w10 { --wi: 9; } .w11 { --wi: 10; } .w12 { --wi: 11; }

@keyframes word-in { from { transform: translateY(128%); } }
@keyframes settle-in { from { transform: translateY(20px); } }
@keyframes fade-in { from { opacity: 0; } }
@keyframes type-in { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0); } }
@keyframes draw-x { from { transform: scaleX(0); } }
@keyframes draw-y { from { transform: scaleY(0); } }
@keyframes shot-in { from { transform: translate3d(72px, 0, 0); } }

/* ---------- First screen: plays on load, from the first paint ---------- */
/* The keyword label types on, the headline rises word by word, and the supporting lines settle into place.
   Those lines and the hero render are visible from the first paint and only move: an entrance that starts
   from invisible would hold back the largest paint of every page by its own length. */
.page-intro h1 small, .hero h1 small { animation: type-in 520ms steps(20, end) backwards; }
.page-intro .w > span, .hero h1 .w > span, .error-page .w > span { animation: word-in 960ms var(--ease-out) backwards; animation-delay: calc(90ms + var(--wi, 0) * 48ms); }
.breadcrumbs { animation: fade-in 420ms linear backwards; }
/* About keeps its headline inside the first column, so that column's own lines are the ones that follow. */
.page-intro > :not(h1, div:first-child), .about-intro > div > :not(h1), .hero-copy > :not(h1), .error-page main > :not(h1, .brand) { animation: settle-in 860ms var(--ease-out) backwards; animation-delay: calc(120ms + var(--k, 0) * 90ms); }
.page-intro > :nth-child(3), .about-intro > div > :nth-child(3), .hero-copy > :nth-child(3), .error-page main > :nth-child(4) { --k: 1; }
.page-intro > :nth-child(4) { --k: 2; }
/* The client render glides the last stretch into place beside the promise. */
.run .hero-shot { animation: shot-in 1300ms var(--ease-out) backwards; }

/* The session track draws itself in: the playhead drops, the line runs out, then each station mark lands. */
.intro .ph-line { animation: draw-y 700ms var(--ease-out) backwards 100ms; }
.intro .ph-head { animation: fade-in 260ms linear backwards 100ms; }
.intro .playhead::before { animation: draw-y 900ms var(--ease-out) backwards 200ms; }
.intro .ruler-line { animation: draw-x 900ms var(--ease-out) backwards 300ms; }
.intro.run .ruler-line { animation: draw-x 2200ms linear backwards 300ms; }
.intro .tick, .intro .ruler-cue { animation: fade-in 300ms linear backwards; animation-delay: calc(500ms + var(--k, 0) * 60ms); }
.tick:nth-child(2) { --k: 1; } .tick:nth-child(3) { --k: 2; } .tick:nth-child(4) { --k: 3; } .tick:nth-child(5) { --k: 4; }
.tick:nth-child(6) { --k: 5; } .tick:nth-child(7) { --k: 6; } .tick:nth-child(8) { --k: 7; } .tick:nth-child(9) { --k: 8; }
.tick:nth-child(10) { --k: 9; } .tick:nth-child(11) { --k: 10; } .tick:nth-child(12) { --k: 11; } .tick:nth-child(13) { --k: 12; }
.ruler-cue { --k: 13; }

/* ---------- Between pages: a short cross-fade, and the product bar holds still ---------- */
@view-transition { navigation: auto; }
.bar { view-transition-name: bar; }
::view-transition-old(root) { animation: 150ms linear both vt-out; }
::view-transition-new(root) { animation: 240ms linear both fade-in; }
@keyframes vt-out { to { opacity: 0; } }

/* ---------- Small feedback ---------- */
/* Plan cards and the review wall's cards lift a little under a fine pointer; touch screens get no hover state. */
@media (hover: hover) and (pointer: fine) {
  .plan, .rating-grid .rating-card { transition: transform 420ms var(--spring), border-color 180ms linear; }
  .plan:hover, .rating-grid .rating-card:hover { transform: translateY(-4px); border-color: var(--outline); }
  .plan-free:hover { border-color: var(--gold); }
}

/* ---------- Below the first screen: each block settles in as it enters (behavior/reveal.js) ---------- */
/* .rv carries the transition only while a block is arriving; reveal.js removes it afterwards, so a block's own
   hover and state transitions are never overridden. --kd staggers blocks that arrive in the same frame. */
.rv { transition: opacity 380ms linear var(--kd, 0ms), transform 720ms var(--spring) var(--kd, 0ms); }
.rv-wait:not([data-kin]) { opacity: 0; transform: translateY(26px); }
.rv-wait .w > span { --in: 0; }
