/* Hoozat — the marketing page.
 *
 * Everything that has a token uses the token. `tokens.css` is generated from
 * Hoozat/design-system/tokens by tools/generate-web-tokens, through the same reader and
 * the same alias resolution that writes the app's Swift, so this file and the app cannot
 * disagree about a colour, a size, a radius, a duration or a curve.
 *
 * The page draws the app's own components rather than pictures of them: `TitleArtwork`,
 * `DSScan`, `DSTile`, `DSCredit`, `DSProfileDisplay`, `DSSearchBar`, the camera's glow
 * and the scan button are all rendered here from the figures their own exports state.
 * Three phone screens are the app at 1:1 in its own 390-point space — see `.screen`.
 *
 * RULE 7 — every literal in this file, and why the token set does not carry it:
 *
 *   1. The `clamp()` growth rates, and `--display-scale`. BOTH BOUNDS OF EVERY CLAMP ARE
 *      TOKENS except the display's upper one, which is `{font.size.display-lg}` times a
 *      factor named once: the type scale was built for a 390-point phone and its top step
 *      is 64, which is a subhead on a 1440 page. A rate of change between two steps, and
 *      how far past the scale's top a desktop hero reaches, are not things a token set
 *      made for an iPhone can name.
 *   2. Optical tracking at display sizes (`--track-display`, `--track-lead`).
 *      `{font.tracking}` is published in absolute px and tops out at 0.49, which is
 *      nothing at 112px; tracking at display sizes is proportional or it is not tracking.
 *   3. The scan button: 72 square at radius 20, its ring a 68 path stroked at 5 — the
 *      `scan-button` layer's own figures in splash.json. The ring sitting half a point
 *      proud of the button is the export's, reproduced rather than corrected.
 *   4. The camera's glow: the artboard's 390 x 844, and the five circles' x, top and
 *      blur. Already rule 7 in the app, for the same reason — an export's geometry.
 *   5. `--raster: 4`. A Gaussian blur is scale-covariant, so a shape drawn at a quarter
 *      and magnified back is the same picture over a sixteenth of the pixels. The app's
 *      own trick; nothing in the token set describes how many pixels to spend on a blur.
 *   6. `TitleArtwork`'s five lights — their centres, radii and blurs as fractions of the
 *      card's height, and the card's own 139 x 209. The export's figures, rule 7 in the
 *      app and rule 7 here.
 *   7. `DSScan`'s 1.44x box, its 24-in-123 bracket ratio and `{radius.focus-ring}`'s
 *      16-in-123, which is how the app scales both.
 *   8. The device's own geometry: a screen of 999 x 2000 at a corner of 8.66% of its
 *      width, which is the sheet render's own measured corner, so the frame and the
 *      picture inside it agree exactly.
 *   9. Loop periods — the spinner's 1.2s, the glow's 18s breath, the marquee's 64s, the
 *      scroll hint's 2.4s. `{motion.duration}` stops at 0.35 and describes a transition
 *      between two states of a control, not a thing that turns for ever.
 *  10. Measures in `ch`. A line length in characters is a property of the typeface.
 *  11. The bento's column spans, and the grain's own frequency and strength.
 *
 * RULE 3 shown on purpose, and greppable: `grep -n 'var(--p-' styles.css`. The layout
 * scale stops at `{space.layout.3xl}` = 64, which is the largest gap the system names,
 * and a page band wants more — so the section rhythm's upper bound is `{space.32}`. The
 * glow's and the artwork's opacities are primitives because that is what their own
 * exports state per shape.
 *
 * RULE 4, said out loud. There is no web design in Figma. Every arrangement below — the
 * hero, the device composition, the marquee, the bento, the steps, the accordion — was
 * decided here. What is NOT invented is anything with a value.
 */

/* ============================================================== faces */
@font-face {
  font-family: "Inter Tight";
  src: url("fonts/InterTight-Medium.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Inter Tight";
  src: url("fonts/InterTight-SemiBold.woff2") format("woff2");
  font-weight: 600; font-style: normal; font-display: swap;
}
/* `font.weight.brand` (Sofia Pro Soft 600) has no file and nothing uses it, so the brand
   face is Bold only — which is `{font.weight.display}` = 700, the weight every brand
   style in the scale asks for. */
@font-face {
  font-family: "Sofia Pro Soft";
  src: url("fonts/SofiaProSoftBold.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
}

/* ======================================================= the derived layer
 * Nothing new is decided here. Each value is a pair of published steps with a growth
 * rate between them, or a published figure restated under a name this page reads by. */
:root {
  colour-scheme: dark;
  color-scheme: dark;

  /* Fluid type. Every bound is a `{font.size}` step; only the display's top is a step
     times a factor, for the reason in rule 7 above. */
  --display-scale: 1.75;
  --type-display: clamp(var(--font-size-heading-xl), 8.4vw,
                        calc(var(--font-size-display-lg) * var(--display-scale)));
  --type-display-sm: clamp(var(--font-size-heading-lg), 5.6vw, var(--font-size-display-lg));
  --type-h2: clamp(var(--font-size-heading-lg), 4vw, var(--font-size-display-sm));
  --type-h3: clamp(var(--font-size-heading-sm), 1.9vw, var(--font-size-heading-md));
  --type-lead: clamp(var(--font-size-body-lg), 1.5vw, var(--font-size-heading-sm));

  /* Optical tracking. See rule 7. */
  --track-display: -0.032em;
  --track-h2: -0.02em;
  --track-lead: -0.01em;

  /* Band rhythm. The layout scale's top is 64; a band between two full-bleed bands wants
     more, so the upper bound reaches past the semantics into `{space.32}`. */
  --section-y: clamp(var(--space-layout-3xl), 9vw, var(--p-space-32));

  /* The glass every native control on this page wears: the ivory pair the app's own
     sheets use, over a backdrop blur at `{elevation.blur.3}`. */
  --glass-fill: var(--colour-background-surface);
  --glass-edge: var(--colour-border-default);
  --glass-blur: var(--elevation-blur-3);
  /* The 1px lit top edge every raised surface in this genre carries. It is the ivory
     ramp's own first step, so it is the system's light rather than a white invented
     here. */
  --lit-edge: inset 0 var(--stroke-default) 0 var(--p-colour-ivory-alpha-1);

  /* The camera's glow artboard, and the quarter-scale raster. Rule 7, both. */
  --glow-board-w: 390;
  --glow-board-h: 844;
  --raster: 4;

  /* The device. 999 x 2000 is the sheet render's own size and 8.66% of its width is its
     own measured corner, so a frame drawn to these holds the picture exactly. */
  --device-aspect: 999 / 2000;
  --device-radius: 8.66%;

  --ease: var(--motion-ease-standard);
}

/* ===================================================================== reset */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--colour-background-page);
  color: var(--colour-text-body);
  font-family: var(--font-family-ui);
  font-weight: var(--font-weight-body);
  font-size: var(--font-size-body-md);
  line-height: var(--font-leading-body);
  letter-spacing: var(--font-tracking-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

img, svg { display: block; max-width: 100%; }
ul, ol { margin: 0; padding: 0; list-style: none; }
h1, h2, h3, p, figure, figcaption { margin: 0; }
a { color: inherit; }

/* RULE 2 held: a link left to itself takes the system accent, a blue this palette has no
   name for, and `{colour.text}` carries no link role. An underline says "this goes
   somewhere" out of the typeface — the app's own answer, in the app's own words. */
.strip a, .faq a, .credit a, .foot a, .ticks a, .ui-sources a {
  text-decoration: underline; text-underline-offset: 0.18em;
  text-decoration-color: var(--p-colour-ivory-alpha-4);
  transition: text-decoration-color var(--motion-duration-sm) var(--ease);
}
.strip a:hover, .faq a:hover, .credit a:hover, .foot a:hover, .ui-sources a:hover {
  text-decoration-color: currentColor;
}

:focus-visible {
  outline: var(--stroke-focus) solid var(--colour-border-solid);
  outline-offset: var(--space-component-xs);
  border-radius: var(--radius-focus-ring);
}

.sr {
  position: absolute; width: 1px; height: 1px; padding: 0;
  margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.skip {
  position: fixed; inset-inline-start: var(--space-layout-sm); inset-block-start: 0;
  transform: translateY(-120%); z-index: 100;
  padding: var(--space-component-sm) var(--space-component-lg);
  background: var(--colour-brand-ivory); color: var(--colour-text-tag-inverse);
  border-radius: var(--radius-button); font-weight: var(--font-weight-label);
  transition: transform var(--motion-duration-sm) var(--ease);
}
.skip:focus { transform: translateY(var(--space-layout-sm)); }

/* ==================================================================== grain
 * A single still frame of monochrome noise over the whole page. Flat navy across a
 * 1440 viewport bands visibly on an 8-bit panel, and a little grain is what stops it —
 * the same reason the genre reaches for it. `pointer-events: none` and a fixed position,
 * so it costs one composited layer and takes part in nothing. */
.grain {
  position: fixed; inset: 0; z-index: 1; pointer-events: none;
  opacity: 0.035; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ==================================================================== layout */
.shell {
  width: 100%; max-width: var(--grid-content-max);
  margin-inline: auto; padding-inline: var(--grid-margin);
}
.shell--prose { max-width: 68ch; }
.section { padding-block: var(--section-y); position: relative; z-index: 2; }
.section--alt { background: var(--colour-background-surface); }
/* A hairline that fades out at both ends rather than running wall to wall. */
.section--alt::before, .section--alt::after {
  content: ""; position: absolute; inset-inline: 0; height: var(--stroke-default);
  background: linear-gradient(90deg,
    var(--p-colour-ivory-alpha-0), var(--p-colour-ivory-alpha-2) 50%, var(--p-colour-ivory-alpha-0));
}
.section--alt::before { inset-block-start: 0; }
.section--alt::after { inset-block-end: 0; }

.sechead { max-width: 24ch; margin-block-end: var(--space-layout-2xl); }
.sechead--wide { max-width: 34ch; }
.sechead--centre { max-width: 28ch; margin-inline: auto; text-align: center; }
.sechead .lead { margin-block-start: var(--space-layout-md); max-width: 46ch; }
.sechead--centre .lead { margin-inline: auto; }

/* ====================================================================== type */
.display {
  font-family: var(--font-family-brand);
  font-weight: var(--font-weight-display);
  font-size: var(--type-display);
  line-height: 0.92;
  letter-spacing: var(--track-display);
  color: var(--colour-text-display);
  text-wrap: balance;
}
.display--sm { font-size: var(--type-display-sm); line-height: 0.96; }
.display em { font-style: normal; color: var(--colour-brand-pink); }

.h2 {
  font-family: var(--font-family-brand);
  font-weight: var(--font-weight-display);
  font-size: var(--type-h2);
  line-height: 1.04;
  letter-spacing: var(--track-h2);
  color: var(--colour-text-heading);
  text-wrap: balance;
}
.h3 {
  font-family: var(--font-family-ui);
  font-weight: var(--font-weight-heading);
  font-size: var(--type-h3);
  line-height: var(--font-leading-heading);
  letter-spacing: var(--track-lead);
  color: var(--colour-text-title);
  text-wrap: balance;
}
.lead {
  font-size: var(--type-lead);
  line-height: 1.55;
  letter-spacing: var(--track-lead);
  color: var(--colour-text-body);
  opacity: var(--p-opacity-70);
  max-width: 44ch;
  text-wrap: pretty;
}

/* A glass pill, which is what this genre puts above a hero headline. */
.eyebrow {
  display: inline-flex; align-items: center; gap: var(--space-component-sm);
  padding: var(--space-component-xs) var(--space-component-lg)
           var(--space-component-xs) var(--space-component-md);
  border-radius: var(--radius-button);
  background: var(--glass-fill);
  border: var(--stroke-default) solid var(--glass-edge);
  box-shadow: var(--lit-edge);
  -webkit-backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur));
  font-size: var(--font-size-label-sm);
  font-weight: var(--font-weight-label);
  letter-spacing: var(--font-tracking-label);
  color: var(--colour-text-label);
}
.eyebrow i {
  width: var(--space-component-sm); height: var(--space-component-sm);
  border-radius: var(--radius-badge); background: var(--colour-brand-gold);
  box-shadow: 0 0 var(--elevation-blur-2) var(--p-colour-gold-alpha-7);
}
.eyebrow--plain {
  display: block; padding: 0; background: none; border: 0; box-shadow: none;
  -webkit-backdrop-filter: none; backdrop-filter: none;
  text-transform: uppercase; color: var(--colour-brand-gold);
  margin-block-end: var(--space-component-lg);
}

/* ================================================================== the mark
 * A CSS mask over `currentColor`, pointed at web/brand/*.svg — which
 * tools/generate-web-brand writes from HoozatMark.swift and HoozatWordmark.swift. The
 * mark's shadow blob carries `fill-opacity` 0.35 in that file and an alpha mask takes it
 * as 35% of the ink, which is exactly what the app draws. Nothing here transcribes a
 * bezier. */
[data-brand] {
  display: block; background-color: currentColor;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain; mask-size: contain;
}
[data-brand="mark"] {
  aspect-ratio: 1;
  -webkit-mask-image: url("brand/mark.svg"); mask-image: url("brand/mark.svg");
}
[data-brand="wordmark"] {
  /* `--wordmark-aspect` is HoozatWordmark.aspect, written into brand/brand.css by the
     same generator — so this page carries no copy of the figure. */
  aspect-ratio: var(--wordmark-aspect);
  -webkit-mask-image: url("brand/wordmark.svg"); mask-image: url("brand/wordmark.svg");
}

.brand {
  display: inline-flex; align-items: center; gap: var(--space-component-sm);
  color: var(--colour-brand-ivory); text-decoration: none;
}
.brand__mark { width: var(--size-icon-lg); }
.brand__word { width: calc(var(--size-icon-lg) * var(--wordmark-aspect)); }
.brand--foot .brand__mark { width: var(--size-icon-xl); }
.brand--foot .brand__word { width: calc(var(--size-icon-xl) * var(--wordmark-aspect)); }

/* =================================================================== buttons
 * The one call to action is prominent and pink, which is `error-sheet`'s own published
 * CTA: a `Glass Effect` filled `{colour.action.primary-default}` at `{radius.button}`
 * with its label in `{colour.text.label}`. The secondary is the glass the app's native
 * controls take above iOS 26 — the same ivory pair, over a backdrop blur. */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--space-component-sm);
  min-height: var(--size-target-min);
  padding-inline: var(--space-control-inset-lg);
  border: 0 solid transparent;
  border-radius: var(--radius-button);
  font-family: var(--font-family-ui);
  font-weight: var(--font-weight-label);
  font-size: var(--font-size-body-md);
  line-height: var(--font-leading-label);
  letter-spacing: var(--font-tracking-label);
  text-decoration: none; cursor: pointer; white-space: nowrap;
  transition: background-color var(--motion-duration-sm) var(--ease),
              transform var(--motion-duration-xs) var(--ease),
              box-shadow var(--motion-duration-md) var(--ease),
              border-color var(--motion-duration-sm) var(--ease);
}
.btn--sm {
  min-height: var(--size-control-sm);
  padding-inline: var(--space-control-inset-sm);
  font-size: var(--font-size-body-sm);
}
.btn--lg { min-height: var(--size-control-lg); padding-inline: var(--space-layout-lg); }

.btn--primary {
  background: var(--colour-action-primary-default);
  color: var(--colour-text-label);
  box-shadow: var(--lit-edge),
    0 var(--elevation-y-3) var(--elevation-blur-4) var(--elevation-spread-3) var(--p-colour-pink-alpha-4);
}
.btn--primary:hover {
  background: var(--colour-action-primary-pressed);
  box-shadow: var(--lit-edge),
    0 var(--elevation-y-4) var(--p-elevation-blur-5) var(--elevation-spread-3) var(--p-colour-pink-alpha-5);
}
.btn--primary:active { transform: scale(0.98); }
/* The lift belongs to the page's own call to action. In a bar it is a halo round a small
   control, which reads as a glitch rather than as depth. */
.btn--sm.btn--primary, .btn--sm.btn--primary:hover { box-shadow: var(--lit-edge); }

.btn--ghost {
  background: var(--glass-fill);
  border-width: var(--stroke-default);
  border-color: var(--glass-edge);
  color: var(--colour-text-label);
  box-shadow: var(--lit-edge);
  -webkit-backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur));
}
.btn--ghost:hover { background: var(--colour-action-pressed); border-color: var(--p-colour-ivory-alpha-3); }
.btn--ghost:active { transform: scale(0.98); }

/* ======================================================================= nav
 * A floating capsule rather than a full-width bar: the page is dark and edge to edge,
 * and a bar drawn across it cuts the hero's light in half. */
.nav {
  position: sticky; inset-block-start: var(--space-component-md); z-index: 50;
  padding-inline: var(--grid-margin);
  margin-block-end: calc(var(--size-control-lg) * -1 - var(--space-component-md));
}
.nav__in {
  display: flex; align-items: center; gap: var(--space-layout-md);
  width: 100%; max-width: var(--grid-content-max); margin-inline: auto;
  padding: var(--space-component-sm) var(--space-component-sm)
           var(--space-component-sm) var(--space-control-inset-sm);
  border-radius: var(--radius-button);
  border: var(--stroke-default) solid transparent;
  transition: background-color var(--motion-duration-md) var(--ease),
              border-color var(--motion-duration-md) var(--ease),
              box-shadow var(--motion-duration-md) var(--ease),
              max-width var(--motion-duration-lg) var(--ease);
}
/* Navy over frosted glass, in that order — the pair PhotoGridSheet's header already
   draws. A material is a blur and an inseparable vibrancy wash, and the navy over it is
   what keeps the result reading as navy rather than as grey. It arrives only once there
   is content behind the bar to separate from. */
.nav.is-stuck .nav__in {
  background: var(--p-colour-navy-alpha-7);
  border-color: var(--glass-edge);
  box-shadow: var(--lit-edge), 0 var(--elevation-y-2) var(--elevation-blur-4) var(--p-colour-navy-alpha-6);
  -webkit-backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur));
}
@media (min-width: 900px) { .nav.is-stuck .nav__in { max-width: 56rem; } }
.nav__links {
  display: none; margin-inline-start: auto; gap: var(--space-layout-md);
  font-size: var(--font-size-body-sm); font-weight: var(--font-weight-label);
}
.nav__links a {
  /* `{size.control.sm}` is 32, which clears WCAG 2.2's 24 for a standalone target —
     these are not inline in a sentence, so the inline exception does not cover them. */
  display: inline-flex; align-items: center; min-height: var(--size-control-sm);
  text-decoration: none; opacity: var(--p-opacity-70);
  transition: opacity var(--motion-duration-sm) var(--ease);
}
.nav__links a:hover { opacity: var(--p-opacity-100); }
.nav .btn { margin-inline-start: auto; }
@media (min-width: 900px) {
  .nav__links { display: flex; }
  .nav .btn { margin-inline-start: 0; }
}

/* ====================================================================== glow
 * The camera screen's five circles. Positions, blurs, colours, opacities and stacking
 * order are the export's, carried on each element as `--x`, `--top`, `--blur`, `--o` and
 * `--hue` — the app's own GlowCircle record, said in CSS.
 *
 * `--up` is `upFromFoot` written out: 390 - (844 - top), which is top - 454. */
.glow { position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
.glow__c {
  position: absolute;
  inset-inline-start: 50%;
  /* One screen down from the field's head, or the field's own foot where that is nearer,
     so a hero exactly a screen tall is the phone exactly. Clipping the field itself to
     one screen instead draws a hard horizontal edge across a bright circle wherever the
     content runs longer than the viewport. */
  inset-block-end: max(0px, calc(100% - 100svh));
  width: calc(var(--glow-side) / var(--raster));
  aspect-ratio: 1; border-radius: var(--radius-badge);
  background: var(--hue); opacity: var(--o);
  /* A filter is applied before the element's transform, so the scale magnifies the blur
     with everything else — hence a quarter here, landing on the export's figure. */
  filter: blur(calc(var(--blur) * var(--glow-unit) / var(--raster)));
  transform-origin: center;
  /* `scale()` is about the centre, so it moves nothing; the translate then puts the
     circle where the export puts it, in unscaled page units, its percentages resolving
     against this box's own width. */
  transform: translate(calc(-50% + var(--x) * var(--glow-unit)),
                       calc(-150% + (var(--top) - 454) * var(--glow-unit)))
             scale(var(--raster));
  /* A slow breath. `translate` is its own property and composes before `transform`, so
     it never fights the placement above — and it is a transform rather than a filter,
     which would re-run the blur on every frame. */
  animation: glow-drift 18s var(--ease) infinite alternate;
  animation-delay: calc(var(--top) * -0.01s);
}
@keyframes glow-drift {
  from { translate: 0 0; }
  to   { translate: 0 calc(var(--space-layout-2xl) * -1); }
}
@media (prefers-reduced-motion: reduce) { .glow__c { animation: none; } }

/* The veil. RULE 4 and the second web-only decision on this page, so it is said out
   loud: on the camera these circles sit over a photograph, with the app's own navy scrim
   under them and a picture under that. A page has no picture, so the same field at the
   same strength does not light the navy — it replaces it, and the page stops having a
   background colour. The veil is the scrim moved on top: navy over the circles where the
   words are, clear where they are not. Its stops are a composition rather than a
   published figure. */
.glow--veiled::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: var(--veil);
}
.hero .glow--veiled { --veil: linear-gradient(to bottom,
  var(--colour-gradient-gradient-100) 0%,
  color-mix(in srgb, var(--colour-brand-navy) 55%, transparent) 38%,
  var(--colour-gradient-gradient-0) 72%); }
.finale .glow--veiled { --veil: linear-gradient(to bottom,
  var(--colour-gradient-gradient-0) 0%,
  var(--p-colour-navy-alpha-7) 32%,
  var(--p-colour-navy-alpha-7) 68%,
  var(--colour-gradient-gradient-0) 100%); }

/* RULE 4, said out loud: the design has never drawn this field anywhere but on a
   390-wide phone, so how far it grows on a desktop was decided here. It is the one
   figure in this block that is not the export's. At a phone's width the field is the
   artboard's own 390 and the picture is the camera's exactly; it then tracks the
   viewport to 1.7 times that. The cap is what keeps the page navy — the upper pair are
   390 across with 150 of blur, so past about 1.7 their spill covers a landscape hero end
   to end and the page stops having a background colour at all. */
.glow--page {
  --glow-reach: clamp(calc(var(--glow-board-w) * 1px), 100vw, calc(var(--glow-board-w) * 1.7px));
  --glow-unit: calc(var(--glow-reach) / var(--glow-board-w));
  --glow-side: var(--glow-reach);
}
/* The finale's is the same arrangement seen from further off. */
.glow--finale {
  --glow-reach: clamp(calc(var(--glow-board-w) * 1px), 100vw, calc(var(--glow-board-w) * 1.9px));
  --glow-unit: calc(var(--glow-reach) / var(--glow-board-w));
  --glow-side: var(--glow-reach);
}
/* Inside a phone screen the field is the artboard exactly: one point is one `--u`. */
.glow--screen { --glow-unit: var(--u); --glow-side: calc(390 * var(--u)); }
.glow--screen .glow__c { inset-block-end: 0; }

/* ====================================================================== hero */
.hero {
  position: relative; isolation: isolate; overflow: clip;
  padding-block: calc(var(--section-y) * 0.7 + var(--size-control-lg) + var(--space-component-md))
                 var(--space-layout-md);
  text-align: center;
}
.hero__in { position: relative; z-index: 2; display: grid; justify-items: center; gap: var(--space-layout-md); }
.hero .display { margin-block: var(--space-component-sm) var(--space-component-xs); }
.hero .lead { max-width: 36ch; }
.hero__cta { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-component-md); }
.hero__meta {
  font-size: var(--font-size-label-sm);
  letter-spacing: var(--font-tracking-label);
  color: var(--colour-text-body);
  opacity: var(--p-opacity-60);
}

/* The device composition. Three screens: the result sheet in the middle, the camera and
   the cast behind it. It is deliberately cut by the fold — the page carries on. */
.stage {
  position: relative; z-index: 2;
  margin-block-start: var(--space-layout-lg);
  display: flex; align-items: flex-end; justify-content: center;
  gap: 0;
  /* The stack tucks under the band below it, so the page's next section starts on a
     device still going — but only far enough that the photograph's credit underneath
     still has the hero to itself. */
  margin-block-end: calc(var(--section-y) * -0.15);
}
.stage__aside { display: none; }
@media (min-width: 900px) {
  .stage__aside { display: block; }
  .stage__aside--left  { transform: rotate(-5deg) translate(12%, 5%); transform-origin: bottom right; }
  .stage__aside--right { transform: rotate(5deg)  translate(-12%, 5%); transform-origin: bottom left; }
  .stage__aside .phone { opacity: var(--p-opacity-90); }
}

/* ================================================================= the device
 * `--pw` is the screen's width in CSS pixels, and `--u` turns it into the app's own
 * point space: everything inside a screen is positioned in the figures the app uses. */
.phone {
  --u: calc(var(--pw) / 390);
  position: relative; width: var(--pw); flex: none;
}
.phone__screen {
  position: relative; overflow: hidden;
  aspect-ratio: var(--device-aspect);
  border-radius: var(--device-radius);
  background: var(--colour-background-page);
  border: var(--stroke-default) solid var(--p-colour-ivory-alpha-2);
  box-shadow:
    var(--lit-edge),
    0 var(--elevation-y-4) var(--p-elevation-blur-6) var(--elevation-spread-4) var(--p-colour-navy-alpha-9);
}
.phone__screen > img { width: 100%; height: 100%; object-fit: cover; }

/* ================================================================ app screens
 * Each one is the app at 1:1 in its own 390-point space. */
.screen { position: absolute; inset: 0; }
.screen > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* The capture overlay's scrim, at the app's own stops: nothing at the head, reaching
   `{opacity.80}` navy at 0.804 of the way down and holding it. It is what the scan button
   is legible against, and it is drawn under the glows exactly as the export has it. */
.screen__wash {
  position: absolute; inset: 0; z-index: 0;
  background: linear-gradient(to bottom,
    var(--colour-gradient-gradient-0) 0%,
    var(--p-colour-navy-alpha-8) 80.4%,
    var(--p-colour-navy-alpha-8) 100%);
}

/* `DSScan`. Focus is four corner brackets with no edge between them; Secondary is the
   outline alone. Both are drawn on a box 1.44x the face, and both scale from the face
   rather than from the box: the brackets take the artboard's 24-in-123 and Secondary
   takes `{radius.focus-ring}`'s 16-in-123, which is the artboard's rule applied twice.
   `--face` is the detected face's own side in points. */
.scanbox {
  --box: calc(var(--face) * 1.44);
  --arm: calc(var(--face) * 24 / 123);
  --pen: calc(var(--face) * 6.75 / 123);
  position: absolute;
  inset-inline-start: calc(var(--cx) * var(--u));
  inset-block-start: calc(var(--cy) * var(--u));
  width: calc(var(--box) * var(--u)); height: calc(var(--box) * var(--u));
  transform: translate(-50%, -50%);
}
.scanbox i {
  position: absolute; width: calc(var(--arm) * var(--u)); height: calc(var(--arm) * var(--u));
  border: calc(var(--pen) * var(--u)) solid var(--colour-brand-ivory);
  /* A bracket's outer edge is an arc of `side - strokeWidth/2` about a centre that far in
     from both edges, which is the same description a rounded rectangle's corner has — so
     the two coincide exactly, and this is `DSCorner.outerRadius` scaled the way the
     brackets are. */
  --r: calc(var(--face) * 20.625 / 123 * var(--u));
  filter: drop-shadow(0 calc(var(--u) * 1) calc(var(--u) * 2) var(--p-colour-navy-alpha-6));
}
.scanbox i:nth-child(1) { inset: 0 auto auto 0; border-width: calc(var(--pen) * var(--u)) 0 0 calc(var(--pen) * var(--u)); border-start-start-radius: var(--r); }
.scanbox i:nth-child(2) { inset: 0 0 auto auto; border-width: calc(var(--pen) * var(--u)) calc(var(--pen) * var(--u)) 0 0; border-start-end-radius: var(--r); }
.scanbox i:nth-child(3) { inset: auto auto 0 0; border-width: 0 0 calc(var(--pen) * var(--u)) calc(var(--pen) * var(--u)); border-end-start-radius: var(--r); }
.scanbox i:nth-child(4) { inset: auto 0 0 auto; border-width: 0 calc(var(--pen) * var(--u)) calc(var(--pen) * var(--u)) 0; border-end-end-radius: var(--r); }

.scanbox--ghost {
  border: calc(var(--stroke-default)) solid var(--colour-border-default);
  border-radius: calc(var(--face) * 16 / 123 * var(--u));
  box-shadow: 0 calc(var(--u) * 1) calc(var(--u) * 2) var(--p-colour-navy-alpha-6);
}

/* The captured state: navy at 70% over a blur, ramping out of the face rather than
   sitting flat — which is what the app's own `ScrimBlur` draws, and what a radial mask is
   exactly.
   **It is a blurred copy of the picture rather than a `backdrop-filter`.** Chromium
   applies a backdrop filter over the element's whole border box and does not mask it, so
   the hole opened in the navy and the blur stayed over the face — which is the one thing
   this state must not do. A second copy of a still picture costs nothing here. */
.screen__scrim {
  position: absolute; inset: 0; overflow: hidden;
  --hole: radial-gradient(circle at calc(var(--cx) * var(--u)) calc(var(--cy) * var(--u)),
            transparent 0, transparent calc(var(--face) * 0.75 * var(--u)),
            #000 calc(var(--face) * 1.5 * var(--u)));
  -webkit-mask-image: var(--hole); mask-image: var(--hole);
}
.screen__scrim img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  filter: blur(calc(var(--u) * 14));
  /* A blur samples beyond its content, so the copy is grown enough that its own soft
     edges stay outside the frame. */
  scale: 1.12;
}
.screen__scrim::after {
  content: ""; position: absolute; inset: 0; background: var(--p-colour-navy-alpha-7);
}

/* The scan button, as splash.json builds it: a 72 square at radius 20 filled ivory at
   10% — the ring is what makes it visible, not the fill — with the 7 October export's 68
   ring stroked at 5 outside it, so 68 + 5 is 73 against the button's 72 and the ring sits
   half a point proud. Reproduced rather than corrected. */
.scanbtn {
  position: absolute; inset-inline-start: 50%; inset-block-end: calc(24 * var(--u));
  translate: -50% 0;
  width: calc(73 * var(--u)); aspect-ratio: 1;
  display: grid; place-items: center; color: var(--colour-brand-ivory);
}
.scanbtn__face {
  grid-area: 1 / 1; place-self: center;
  width: calc(72 * var(--u)); height: calc(72 * var(--u));
  border-radius: calc(20 * var(--u));
  background: var(--p-colour-ivory-alpha-1);
  box-shadow: inset 0 0 0 calc(var(--u) * 1) var(--p-colour-ivory-alpha-2);
}
.scanbtn__ring { grid-area: 1 / 1; place-self: center; overflow: visible; width: 100%; height: 100%; }
/* The arc travels round the ring; the ring itself never moves. That is what
   captured.json's "Moving Angular Stroke" is — a trim animating around a fixed path —
   and it is also the only version that reads right, a rounded square spun bodily looking
   like a wheel somebody has put a corner on. `pathLength="100"` makes the offset a
   percentage of the perimeter, so one turn is one -100. */
.is-spinning .scanbtn__ring rect { animation: ring-trim 1.2s linear infinite; }
@keyframes ring-trim { to { stroke-dashoffset: -100; } }
@media (prefers-reduced-motion: reduce) { .is-spinning .scanbtn__ring rect { animation: none; } }

/* The two glass icon buttons, 24 in from each bottom corner. */
.screen__ctl {
  position: absolute; inset-block-end: calc(24 * var(--u));
  width: calc(44 * var(--u)); height: calc(44 * var(--u));
  display: grid; place-items: center;
  border-radius: var(--radius-badge);
  background: var(--glass-fill);
  border: var(--stroke-default) solid var(--glass-edge);
  box-shadow: var(--lit-edge);
  -webkit-backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur));
  color: var(--colour-icon-icon);
}
.screen__ctl svg { width: calc(20 * var(--u)); height: calc(20 * var(--u)); }
.screen__ctl--left  { inset-inline-start: calc(24 * var(--u)); }
.screen__ctl--right { inset-inline-end:   calc(24 * var(--u)); }

/* `DSTag` Secondary, in the slot under the shutter the app's own hints use. */
.screen__hint {
  position: absolute; inset-inline-start: 50%; translate: -50% 0;
  inset-block-end: calc((24 + 73 + 16) * var(--u));
  padding: calc(4 * var(--u)) calc(8 * var(--u));
  border-radius: calc(4 * var(--u));
  background: var(--colour-background-surface);
  border: var(--stroke-default) solid var(--colour-border-default);
  -webkit-backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur));
  color: var(--colour-text-tag);
  font-size: calc(12 * var(--u)); font-weight: var(--font-weight-label);
  white-space: nowrap;
}

/* ============================================================== TitleArtwork
 * Both cards the app draws — the carousel's 139 x 209 and the credit row's 36 x 54 — are
 * one composition at two sizes: the title's `base` colour, a vertical gradient down to
 * its `identity1`, and five soft lights over that. Every figure below is a fraction of
 * the card's own height, which is how the export states them, so one `--h` scales the
 * whole drawing.
 *
 * The six colours come down the wire on every credit as `Credit.colours`, and the
 * palettes on this page are real ones. */
.art {
  position: relative; overflow: hidden; isolation: isolate;
  width: calc(var(--h) * 139 / 209); height: var(--h);
  /* `{radius.card}` and `{radius.4}` as fractions of the height, so one `--h` scales the
     corner with everything else — the publish gives the large card 16 and the poster 4. */
  border-radius: calc(var(--h) * 16 / 209);
  background: color-mix(in srgb, var(--base) 60%, transparent);
  border: var(--stroke-default) solid var(--colour-border-default);
}
.art::before {
  content: ""; position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(to bottom, color-mix(in srgb, var(--base) 20%, transparent), var(--identity1));
  opacity: var(--p-opacity-70);
}
/* Each light is blurred on its own and then dimmed, in that order — alpha compositing is
   not linear, so dimming before the blur is a different sum wherever two lights overlap.
   Drawn at a quarter and magnified back, as every blur in the app is. */
.art i {
  position: absolute; z-index: 2; border-radius: var(--radius-badge);
  width: calc(var(--rx) * 2 * var(--h) / var(--raster));
  height: calc(var(--ry) * 2 * var(--h) / var(--raster));
  inset-inline-start: calc(var(--cx) * var(--h));
  inset-block-start: calc(var(--cy) * var(--h));
  background: var(--c); opacity: var(--o);
  filter: blur(calc(var(--b) * var(--h) / var(--raster)));
  transform: translate(-50%, -50%) scale(var(--raster));
}
.art__text {
  position: absolute; inset: 0; z-index: 3;
  display: flex; flex-direction: column; justify-content: space-between;
  padding: calc(var(--h) * 24 / 209);
  color: var(--colour-brand-ivory);
  font-family: var(--font-family-brand);
  font-weight: var(--font-weight-display);
  letter-spacing: var(--font-tracking-display);
}
.art__title { font-size: calc(var(--h) * 18 / 209); line-height: 1.15; }
.art__year  { font-size: calc(var(--h) * 16 / 209); }

/* The credit poster: the same composition at 36 x 54, with no words in it — the publish
   declares no text role on Small at all — and the app's mark over it. */
.art--poster { width: calc(var(--h) * 36 / 54); border-radius: calc(var(--h) * 4 / 54); }
.art--poster .art__mark {
  position: absolute; z-index: 3; inset-inline-start: 50%; inset-block-start: 50%;
  translate: -50% -50%;
  width: calc(var(--h) * 16 / 54); color: var(--colour-brand-ivory);
  opacity: var(--p-opacity-60);
}

/* The result sheet's own filmography card, drawn in the phone's point space so every
   figure is the one the app uses. */
.screen--sheet {
  display: flex; align-items: center;
  padding: calc(24 * var(--u));
  background: var(--colour-background-page);
}
.screen--sheet::before {
  content: ""; position: absolute; inset-inline: calc(-20 * var(--u)); inset-block-end: calc(-120 * var(--u));
  height: calc(420 * var(--u)); z-index: 0;
  background: radial-gradient(60% 60% at 50% 100%, var(--p-colour-pink-alpha-4), transparent 70%);
  filter: blur(calc(40 * var(--u)));
}
.sheetpane {
  position: relative; z-index: 1; width: 100%;
  display: grid; gap: calc(16 * var(--u));
  padding: calc(24 * var(--u));
  border-radius: calc(16 * var(--u));
  background: var(--colour-background-surface);
  border: var(--stroke-default) solid var(--colour-border-default);
  box-shadow: var(--lit-edge);
  -webkit-backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur));
}
.sheetpane__head { display: flex; align-items: center; justify-content: space-between; gap: calc(16 * var(--u)); }
.sheetpane__title {
  font-family: var(--font-family-brand); font-weight: var(--font-weight-display);
  font-size: calc(24 * var(--u)); color: var(--colour-text-title);
  letter-spacing: var(--font-tracking-display);
}
.sheetpane__rows { display: grid; gap: calc(12 * var(--u)); }
.screen--sheet .ui-credit { gap: calc(16 * var(--u)); }
.screen--sheet .ui-credit__text { gap: calc(4 * var(--u)); }
.screen--sheet .ui-credit__title { font-size: calc(16 * var(--u)); }
.screen--sheet .ui-credit__sub { font-size: calc(14 * var(--u)); }
.screen--sheet .tag {
  font-size: calc(12 * var(--u));
  padding: calc(4 * var(--u)) calc(8 * var(--u));
  border-radius: calc(4 * var(--u));
}

/* =================================================================== marquee
 * A full-bleed row of real title cards, in the six colours the service gives each one.
 * Duplicated once so the loop has no seam; the copy is hidden from the reader. */
.marquee-band { padding-block: var(--section-y); position: relative; z-index: 2; }
.marquee { position: relative; overflow: hidden; }
.marquee__track {
  display: flex; gap: var(--space-layout-xs); width: max-content;
  animation: marquee 64s linear infinite;
}
.marquee:hover .marquee__track { animation-play-state: paused; }
@keyframes marquee { to { transform: translateX(calc(-50% - var(--space-layout-xs) / 2)); } }
@media (prefers-reduced-motion: reduce) {
  .marquee__track { animation: none; }
  .marquee { overflow-x: auto; scrollbar-width: none; }
}
/* The row dissolves into the page at both edges rather than being cut by one. */
.marquee::before, .marquee::after {
  content: ""; position: absolute; inset-block: 0; width: clamp(4rem, 22vw, 20rem); z-index: 2;
  pointer-events: none;
}
.marquee::before { inset-inline-start: 0;  background: linear-gradient(90deg, var(--colour-gradient-gradient-100), var(--colour-gradient-gradient-0)); }
.marquee::after  { inset-inline-end: 0;    background: linear-gradient(270deg, var(--colour-gradient-gradient-100), var(--colour-gradient-gradient-0)); }
.marquee__note {
  position: relative; z-index: 3; text-align: center;
  margin-block-start: var(--space-layout-xl);
  margin-inline: auto; max-width: 52ch;
  font-size: var(--font-size-body-md); opacity: var(--p-opacity-70);
}

/* ===================================================================== strip */
.strip { position: relative; z-index: 2; padding-block: var(--space-layout-xl); }
.strip__in { display: grid; gap: var(--space-layout-md); }
.strip__lede {
  font-size: var(--type-lead); font-weight: var(--font-weight-heading);
  letter-spacing: var(--track-lead); color: var(--colour-text-heading); max-width: 30ch;
}
.strip__list { display: flex; flex-wrap: wrap; gap: var(--space-layout-lg); }
.strip__list li { display: grid; gap: var(--space-component-xs); }
.strip__list span {
  font-size: var(--font-size-label-sm); letter-spacing: var(--font-tracking-label);
  text-transform: uppercase; opacity: var(--p-opacity-60);
}
.strip__list strong {
  font-size: var(--font-size-body-lg); font-weight: var(--font-weight-heading);
  color: var(--colour-text-title);
}
@media (min-width: 900px) {
  .strip__in { grid-template-columns: auto 1fr; align-items: center; gap: var(--space-layout-3xl); }
  .strip__list { justify-content: flex-end; }
}

/* ===================================================================== steps */
.steps { display: grid; gap: var(--space-layout-2xl); }
.step { display: grid; gap: var(--space-layout-md); justify-items: start; }
.step__shot { justify-self: center; }
.step__n {
  display: inline-flex; align-items: center; gap: var(--space-component-sm);
  font-family: var(--font-family-brand); font-weight: var(--font-weight-display);
  font-size: var(--font-size-heading-sm); color: var(--colour-brand-pink);
  letter-spacing: var(--font-tracking-display);
}
.step__n::after {
  content: ""; width: var(--space-layout-lg); height: var(--stroke-default);
  background: linear-gradient(90deg, var(--p-colour-pink-alpha-6), var(--p-colour-pink-alpha-0));
}
.step p { opacity: var(--p-opacity-70); max-width: 38ch; }
@media (min-width: 900px) {
  .steps { grid-template-columns: repeat(3, 1fr); gap: var(--space-layout-lg); align-items: start; }
  /* The three screens sit at different heights, so the row reads as a composition rather
     than as a table. */
  .step:nth-child(2) { margin-block-start: var(--space-layout-lg); }
  .step:nth-child(3) { margin-block-start: var(--space-layout-xs); }
}

/* ===================================================================== bento */
.bento { display: grid; gap: var(--space-layout-md); }
.card {
  position: relative; overflow: hidden; isolation: isolate;
  display: flex; flex-direction: column; gap: var(--space-component-md);
  padding: var(--space-layout-md);
  border: var(--stroke-default) solid var(--colour-border-default);
  border-radius: var(--radius-card);
  background: var(--colour-background-surface);
  box-shadow: var(--lit-edge);
  transition: border-color var(--motion-duration-md) var(--ease),
              transform var(--motion-duration-md) var(--ease);
}
/* A faint light from the card's own top-left corner, which is what gives a flat surface
   on a flat page any depth at all. At `z-index: -1` it painted behind the card's own
   background and was invisible; it sits at 0 with the content above it. */
.card::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: radial-gradient(90% 70% at 0% 0%, var(--p-colour-ivory-alpha-1), transparent 72%);
}
.card > * { position: relative; z-index: 1; }
.card:hover { border-color: var(--p-colour-ivory-alpha-4); transform: translateY(calc(var(--space-component-xs) * -1)); }
.card p { opacity: var(--p-opacity-70); max-width: 42ch; }
.card__art { margin-block-start: auto; padding-block-start: var(--space-layout-md); }
@media (min-width: 768px) {
  .bento { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .card { padding: var(--space-layout-lg); }
}
@media (min-width: 1024px) {
  /* Six tracks and six cards, in three rows that each fill exactly: 4 + 2, then
     2 + 2 + 2, then one across. Spans rather than areas, so the order in the markup is
     the reading order. */
  .bento { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .card { grid-column: span 2; }
  .card--wide { grid-column: span 4; }
  .card--full { grid-column: span 6; }
  .card--split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); gap: var(--space-layout-lg); align-items: center; }
  .card--split .card__art { margin-block-start: 0; padding-block-start: 0; }
  .card--split > div { display: grid; gap: var(--space-component-md); align-content: center; }
}

/* ============================================== the app's own parts, in miniature
 * Each fragment below is a real component rendered from its own tokens rather than a
 * drawing of one, which is the same rule the phone screens follow. */
.ui {
  display: grid; gap: var(--space-component-md);
  padding: var(--space-component-lg);
  border-radius: var(--radius-card-container);
  background: var(--p-colour-navy-alpha-5);
  border: var(--stroke-default) solid var(--colour-border-default);
  box-shadow: var(--lit-edge);
}

/* `DSCredit` — the filmography row: the Small tile, the title and year, and a tag. */
.ui-credit { display: flex; align-items: center; gap: var(--space-component-lg); }
.ui-credit__text { display: grid; gap: var(--space-component-xs); min-width: 0; }
.ui-credit__title {
  font-family: var(--font-family-brand); font-weight: var(--font-weight-display);
  font-size: var(--font-size-body-md); color: var(--colour-text-title);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ui-credit__sub { font-size: var(--font-size-body-sm); opacity: var(--p-opacity-60); }
.ui-credit .tag { margin-inline-start: auto; }

/* `DSTag`. */
.tag {
  flex: none;
  padding: var(--space-component-xs) var(--space-component-sm);
  border-radius: var(--radius-tag);
  background: var(--colour-background-surface);
  border: var(--stroke-default) solid var(--colour-border-default);
  font-size: var(--font-size-label-sm); font-weight: var(--font-weight-label);
  color: var(--colour-text-tag); white-space: nowrap;
}
.tag--primary { background: var(--colour-brand-ivory); color: var(--colour-text-tag-inverse); border-color: transparent; }

/* `DSSearchBar`. The spec's two variants are byte-identical in all thirteen styles and
   differ only in their label, so a filled bar looks like an empty one. */
.ui-bar {
  display: flex; align-items: center; gap: var(--space-component-md);
  min-height: var(--size-control-lg);
  padding-inline: var(--space-control-inset-sm);
  border-radius: var(--radius-button);
  background: var(--colour-background-surface);
  border: var(--stroke-default) solid var(--colour-border-default);
  box-shadow: var(--lit-edge);
  color: var(--colour-text-display); font-size: var(--font-size-body-md);
}
.ui-bar svg { flex: none; width: var(--size-icon-md); height: var(--size-icon-md); opacity: var(--p-opacity-60); }
.ui-bar span { opacity: var(--opacity-disabled); }
.ui-bar--typed span { opacity: var(--p-opacity-100); }
.ui-bar__caret {
  width: var(--p-stroke-2); height: var(--font-size-body-lg);
  background: var(--colour-brand-ivory); margin-inline-start: calc(var(--space-component-xs) * -1);
  animation: caret 1.1s steps(1) infinite;
}
@keyframes caret { 50% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .ui-bar__caret { animation: none; } }

/* `DSProfileDisplay` Small — the history and cast row: a 36pt avatar, the name, and a
   chevron. The avatar's Placeholder is the brand ramp with the mark over it, which is
   what its own SVG export draws. */
.ui-row { display: flex; align-items: center; gap: var(--space-layout-xs); }
.ui-row__face {
  /* 36 is `profile-image` Small's own published side since 5 October, and `{size.icon}`
     names 32 and 40 and nothing between. Rule 7, and the same figure the app reads. */
  flex: none; position: relative; width: 36px; height: 36px;
  /* `{radius}` names nothing at a quarter of 36, so this is rule 7 — the design holds a
     quarter of the side, which the 8-at-32 and 9-at-36 publishes say between them. */
  border-radius: 25%;
  background: linear-gradient(161deg, var(--colour-brand-coral), var(--colour-brand-pink));
  overflow: hidden;
}
.ui-row__face img { width: 100%; height: 100%; object-fit: cover; }
.ui-row__face [data-brand] {
  position: absolute; inset-inline-start: 50%; inset-block-start: 50%;
  translate: -50% -50%; width: 45%; color: var(--colour-brand-ivory);
}
.ui-row__text { display: grid; gap: calc(var(--space-component-xs) / 2); min-width: 0; }
.ui-row__name {
  font-size: var(--font-size-body-md); font-weight: var(--font-weight-label);
  color: var(--colour-text-label);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ui-row__sub { font-size: var(--font-size-body-sm); opacity: var(--p-opacity-60); }
/* A row whose name is nobody's. The history is a real list of real people, and a mockup
   is the one place this page could put words in somebody's mouth — so the rows past the
   first carry the published Placeholder avatar and the shape of a name, and no name. */
.ui-skeleton {
  display: block; width: var(--w); height: var(--space-component-sm);
  border-radius: var(--radius-badge); background: var(--p-colour-ivory-alpha-2);
}
.ui-skeleton + .ui-skeleton { height: var(--space-component-xs); background: var(--p-colour-ivory-alpha-1); }
.ui-row__chev { margin-inline-start: auto; opacity: var(--p-opacity-40); flex: none; }
.ui-row__chev svg { width: var(--size-icon-sm); height: var(--size-icon-sm); }
.ui-divider { height: var(--stroke-default); background: var(--colour-border-default); }

/* The filmography's filter, a native `Menu` on the sheet. */
.ui-menu { display: grid; gap: var(--space-component-xs); }
.ui-menu__row {
  display: flex; align-items: center; gap: var(--space-component-md);
  padding: var(--space-component-sm) var(--space-component-md);
  border-radius: var(--radius-input);
  font-size: var(--font-size-body-sm);
}
.ui-menu__row--on { background: var(--colour-action-pressed); color: var(--colour-text-label); }
.ui-menu__row--on::after { content: "✓"; margin-inline-start: auto; }

/* `PhotoGridSheet` — four columns, a 2pt gutter, and the chosen cell bracketed with the
   rest dimmed. */
.ui-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--p-stroke-2); border-radius: var(--radius-media); overflow: hidden; }
.ui-grid span { aspect-ratio: 1; background: var(--p-colour-ivory-alpha-3); position: relative; }
.ui-grid span:nth-child(3n)   { background: var(--p-colour-ivory-alpha-5); }
.ui-grid span:nth-child(4n+1) { background: var(--p-colour-pink-alpha-4); }
.ui-grid span:nth-child(5n)   { background: var(--p-colour-gold-alpha-4); }
.ui-grid span:nth-child(8n)   { background: var(--p-colour-coral-alpha-3); }
/* Everything but the chosen cell dims to `{colour.brand.navy}` at `{opacity.overlay}` —
   the same wash the camera's scrim is made of — and the chosen one is bracketed with
   `DSScan` inset by `{space.component.xs}`, so the brackets sit *in* the photograph
   rather than on its edge. */
.ui-grid span::after {
  content: ""; position: absolute; inset: 0; z-index: 1;
  background: var(--colour-brand-navy); opacity: var(--opacity-overlay);
}
.ui-grid span.is-picked::after { content: none; }
.ui-grid i {
  position: absolute; z-index: 2; width: 26%; height: 26%;
  border: var(--stroke-focus) solid var(--colour-brand-ivory);
}
.ui-grid i:nth-child(1) { inset: var(--space-component-xs) auto auto var(--space-component-xs); border-width: var(--stroke-focus) 0 0 var(--stroke-focus); border-start-start-radius: var(--radius-tag); }
.ui-grid i:nth-child(2) { inset: var(--space-component-xs) var(--space-component-xs) auto auto; border-width: var(--stroke-focus) var(--stroke-focus) 0 0; border-start-end-radius: var(--radius-tag); }
.ui-grid i:nth-child(3) { inset: auto auto var(--space-component-xs) var(--space-component-xs); border-width: 0 0 var(--stroke-focus) var(--stroke-focus); border-end-start-radius: var(--radius-tag); }
.ui-grid i:nth-child(4) { inset: auto var(--space-component-xs) var(--space-component-xs) auto; border-width: 0 var(--stroke-focus) var(--stroke-focus) 0; border-end-end-radius: var(--radius-tag); }

/* The Sources sheet behind the footer's link. */
.ui-sources { display: grid; gap: var(--space-component-md); font-size: var(--font-size-body-sm); }
.ui-sources li { opacity: var(--p-opacity-70); }
.ui-sources b { display: block; font-weight: var(--font-weight-label); color: var(--colour-text-label); opacity: var(--p-opacity-100); }

/* =================================================================== privacy */
.privacy { display: grid; gap: var(--space-layout-xl); align-items: center; }
.ticks { display: grid; gap: var(--space-component-lg); margin-block-start: var(--space-layout-md); max-width: 46ch; }
.ticks li { position: relative; padding-inline-start: var(--space-layout-md); opacity: var(--p-opacity-70); }
.ticks li::before {
  content: ""; position: absolute; inset-inline-start: 0; inset-block-start: 0.55em;
  width: var(--space-component-sm); height: var(--space-component-sm);
  border-radius: var(--radius-badge); background: var(--colour-brand-gold);
  box-shadow: 0 0 var(--elevation-blur-2) var(--p-colour-gold-alpha-5);
}
.note {
  margin-block-start: var(--space-layout-lg);
  padding: var(--space-layout-md);
  border-inline-start: var(--stroke-strong) solid var(--colour-brand-coral);
  border-radius: var(--radius-card-container);
  background: var(--colour-background-surface);
  box-shadow: var(--lit-edge);
  font-size: var(--font-size-body-sm); max-width: 50ch;
}
.privacy__art { display: grid; place-items: center; }
.shield {
  display: grid; place-items: center;
  width: min(60vw, 260px); aspect-ratio: 1;
  border-radius: var(--radius-badge);
  background: radial-gradient(circle at 50% 50%, var(--p-colour-pink-alpha-3), var(--p-colour-pink-alpha-0) 70%);
  border: var(--stroke-default) solid var(--colour-border-default);
  box-shadow: var(--lit-edge);
}
.shield__mark { width: 40%; color: var(--colour-brand-ivory); }
@media (min-width: 900px) { .privacy { grid-template-columns: 1.2fr 0.8fr; } }

/* ======================================================================= faq */
.faq { display: grid; gap: var(--space-component-md); }
.faq details {
  border: var(--stroke-default) solid var(--colour-border-default);
  border-radius: var(--radius-card);
  background: var(--colour-background-surface);
  box-shadow: var(--lit-edge);
  padding: var(--space-layout-sm) var(--space-layout-md);
  transition: border-color var(--motion-duration-md) var(--ease);
}
.faq details[open] { border-color: var(--p-colour-ivory-alpha-4); }
.faq summary {
  cursor: pointer; list-style: none;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-component-lg);
  min-height: var(--size-target-min);
  font-weight: var(--font-weight-heading);
  font-size: var(--font-size-body-lg);
  letter-spacing: var(--track-lead);
  color: var(--colour-text-title);
}
.faq summary::-webkit-details-marker { display: none; }
/* A chevron drawn from two borders rather than an icon: `icons.json` publishes a box, a
   fill and a name and no path data at all, so there is no Down glyph to use. */
.faq summary::after {
  content: ""; flex: none;
  width: var(--size-icon-sm); height: var(--size-icon-sm);
  border-inline-end: var(--stroke-focus) solid currentColor;
  border-block-end: var(--stroke-focus) solid currentColor;
  rotate: 45deg; translate: 0 calc(var(--space-component-xs) * -0.5);
  transition: rotate var(--motion-duration-md) var(--ease);
}
.faq details[open] summary::after { rotate: 225deg; translate: 0 var(--space-component-xs); }
.faq details p { padding-block: var(--space-component-md) var(--space-component-lg); opacity: var(--p-opacity-70); max-width: 62ch; }

/* ==================================================================== finale */
.finale {
  position: relative; isolation: isolate; overflow: hidden;
  padding-block: calc(var(--section-y) * 1.15);
  text-align: center;
  border-block-start: var(--stroke-default) solid var(--colour-border-default);
}
.finale__in { position: relative; z-index: 2; display: grid; justify-items: center; gap: var(--space-layout-md); }
.finale__mark { width: var(--size-icon-2xl); color: var(--colour-brand-ivory); }
.finale .lead { max-width: 36ch; }
.finale .hero__meta { margin-block-start: 0; }

/* ==================================================================== footer */
.foot {
  position: relative; z-index: 2;
  padding-block: var(--space-layout-2xl);
  border-block-start: var(--stroke-default) solid var(--colour-border-default);
  background: var(--colour-background-surface);
}
.foot__in { display: grid; gap: var(--space-layout-lg); }
.foot__h {
  font-size: var(--font-size-label-sm); font-weight: var(--font-weight-label);
  letter-spacing: var(--font-tracking-label); text-transform: uppercase;
  color: var(--colour-text-body);
  /* NOT `{opacity.disabled}`. That semantic means a disabled control, and at 40% ivory
     over this surface it measures 3.2:1 — under AA for text of any size. */
  opacity: var(--p-opacity-60);
  margin: 0 0 var(--space-component-lg);
}
.foot__sources ul { display: grid; gap: var(--space-component-md); max-width: 60ch; }
.foot__sources li { font-size: var(--font-size-body-sm); opacity: var(--p-opacity-70); }
.foot__fine { font-size: var(--font-size-label-sm); opacity: var(--p-opacity-60); }
@media (min-width: 900px) {
  .foot__in { grid-template-columns: auto 1fr; align-items: start; gap: var(--space-layout-3xl); }
  .foot__fine { grid-column: 1 / -1; }
}

/* The photograph's own credit. CC BY-SA asks for attribution "in a manner reasonable to
   the medium", which on a web page means legible — so this is `{font.size.label-md}` at
   `{opacity.60}` rather than the smallest step at the faintest. */
.credit {
  margin-block-start: var(--space-layout-lg);
  font-size: var(--font-size-label-md);
  line-height: var(--font-leading-label);
  text-align: center; color: var(--colour-text-body);
  opacity: var(--p-opacity-60);
}

/* =================================================================== reveals
 * Travel is a layout token, so the distance a thing moves is on the spacing scale rather
 * than typed. Reduce Motion keeps the fade and drops the movement, which is the app's
 * own rule everywhere. */
.reveal {
  opacity: 0; transform: translateY(var(--space-layout-md));
  transition: opacity var(--motion-duration-lg) var(--ease),
              transform var(--motion-duration-lg) var(--ease);
}
.reveal.is-in { opacity: 1; transform: none; }
/* Children of a group marked `data-stagger` arrive a beat behind each other, which is
   what stops a row of six cards reading as one slab. The delay is set in site.js, where
   the member's own index is known, and cleared once it has been spent. */
/* No JavaScript, no reveal: everything is simply there. */
.no-js .reveal { opacity: 1; transform: none; transition: none; }
@media (prefers-reduced-motion: reduce) { .reveal { transform: none; } }

/* The hero's devices drift as the page leaves them, where the browser can drive an
   animation off the scroll itself. Unsupported, they simply sit still. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) and (min-width: 900px) {
    [data-parallax] {
      animation: drift linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }
    @keyframes drift {
      from { transform: translateY(var(--space-layout-xl)); }
      to   { transform: translateY(calc(var(--space-layout-xl) * -1)); }
    }
  }
}

@media print {
  .glow, .grain, .hero__scroll, .scanbtn, .marquee { display: none; }
  body { background: #fff; color: #000; }
}
