/* ==========================================================================
   GENESI, site.css
   The whole design system for luxim.app. One file, no framework, no build.

   Owned by the marketing-site agent. No other stylesheet exists. Any page
   added later must be renderable with the classes documented below and
   NOTHING ELSE: if a page needs a style that is not here, the style belongs
   here, not in a <style> block on the page.

   --------------------------------------------------------------------------
   REUSABLE CLASSES FOR PAGES THIS FILE'S AUTHOR DID NOT WRITE
   (privacy, terms, support, and anything else long-form)
   --------------------------------------------------------------------------

   .page-shell   The outer wrapper. One per page, a direct child of <body>,
                 wrapping header + main + footer. Gives the page a full-height
                 column so the footer sits at the bottom of short pages.
                 It also styles its own `> main`: centred, max-width 72rem,
                 gutters, and vertical rhythm. No class needed on <main>.

   .site-header  The top bar. Flex row, logo left, nav right, seam underneath.
                 Wraps to two rows under about 30rem. Expected markup:
                   <header class="site-header">
                     <a class="wordmark" href="/">
                       <img class="logo-mark" src="..." alt="" width="36" height="36">
                       Luxim
                     </a>
                     <nav><a href="/">Overview</a><a href="/support">Support</a></nav>
                   </header>
                 Both .wordmark and the <nav> links are styled by this file.

   .site-footer  The bottom bar. Seam above, small type, wrapping link row.
                 Expected markup:
                   <footer class="site-footer">
                     <p>Luxim</p>
                     <nav><a href="/privacy">Privacy</a>...</nav>
                   </footer>

   .page-title   The h1 of a content page. Display face, tight tracking.
   .page-meta    The line under a .page-title, typically "Last updated ...".
                 Small, secondary colour, letter-spaced.

   .prose        Long-form body copy. Put it on one wrapper <div> or <article>
                 and write plain HTML inside. Styled descendants:
                 h2, h3, h4, p, ul, ol, li, nested lists, a, strong, em,
                 blockquote, hr, code, pre, table/thead/tbody/tr/th/td,
                 dl/dt/dd, and `:first-child` margin collapse. Tables scroll
                 horizontally inside themselves, so a wide table can never
                 push the page sideways. Measure is capped at 68ch.

   .card         An elevated panel on the Cinema Glow surface, 1px seam,
                 rounded. Safe inside .prose (an aside, a summary box) and
                 outside it.

   .btn          Secondary action or link-as-button. Bordered, no fill.
   .btn-amber    THE primary action. Amber-to-Solar fill, Onyx text.
                 Amber is reserved: use .btn-amber for the one call to action
                 that matters on a page, and .btn for everything else.

   Utility classes also available anywhere: .eyebrow, .lede, .footnote,
   .todo, .stack (vertical rhythm), .visually-hidden, .center.

   --------------------------------------------------------------------------
   COLOUR AND CONTRAST
   --------------------------------------------------------------------------
   Palette is the brand blueprint's, unchanged (docs/genesi-brand.md §2).
   Contrast measured against the app ground #0A0A0C (Palette.background).
   Recomputed for this palette, the accent moved from 50% to 71% lightness,
   so every pair that touches it changed. Ratios are WCAG 2.1.

     Stone 100  #F5F3F0 on ink         17.86 : 1   AAA
     Stone 400  #9A968F on ink          6.72 : 1   AA
     Stone 600  #615E59 on ink          3.06 : 1   AA large only
     Gold       #F2C078 on ink         11.86 : 1   AAA
     Gold deep  #D99A48 on ink          8.16 : 1   AAA
     Ink        #0A0A0C on gold        11.86 : 1   AAA
     Ink        #0A0A0C on gold-deep    8.16 : 1   AAA
     Stone 100  #F5F3F0 on surface-1   16.29 : 1   AAA
     Stone 400  #9A968F on surface-1    6.13 : 1   AA
     Stone 400  #9A968F on surface-2    5.63 : 1   AA
     Danger     #E0685E on ink          5.94 : 1   AA
     Positive   #7FBF8F on ink          9.19 : 1   AAA

   The gold is now LIGHT, so it carries dark text well and is itself highly
   legible on the ground. Stone 600 is decoration only and must never carry
   meaning.

   Lunar Grey clears AA at every size used here, so no lightening was needed.
   Secondary type is never set below 0.8125rem / 13px anywhere in this file.

   --------------------------------------------------------------------------
   MOTION
   --------------------------------------------------------------------------
   Three animations exist: a slow ambient glow behind the hero mark, a
   fade-and-rise on first paint, and hover/focus transitions. All three are
   cancelled by the prefers-reduced-motion block at the end of this file.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. TOKENS
   -------------------------------------------------------------------------- */

:root {
  /* Palette, raw, mirrors ios/Lumen/Design/DesignSystem.swift line for line.
     The app is the source of truth for every value below. Warm on purpose:
     "a blue-black looks like a spreadsheet" (DesignSystem.swift:14). */
  --ink: #0a0a0c;            /* Palette.background      :19 */
  --surface-1: #16161a;      /* Palette.surface         :22 */
  --surface-2: #1e1e24;      /* Palette.surfaceHigh     :23 */
  --gold: #f2c078;           /* Palette.accent          :27 */
  --gold-deep: #d99a48;      /* Palette.accentDeep      :28 */
  --stone-100: #f5f3f0;      /* Palette.textPrimary     :30 */
  --stone-400: #9a968f;      /* Palette.textSecondary   :31 */
  --stone-600: #615e59;      /* Palette.textFaint       :32 */
  --danger: #e0685e;         /* Palette.danger          :34 */
  --positive: #7fbf8f;       /* Palette.positive        :35 */

  /* Palette, semantic. The accent is spent on exactly three things, the
     primary action, the current selection, and progress (DesignSystem.swift:26).
     Nothing decorative gets gold. */
  --bg: var(--ink);
  --text: var(--stone-100);
  --text-secondary: var(--stone-400);
  --text-faint: var(--stone-600);          /* decoration only, 3.06:1 */
  --accent: var(--gold);
  --accent-deep: var(--gold-deep);
  /* Top-to-bottom, as the app runs it. A 135deg sweep travels much further
     across a wide web button than across a 358pt phone one. */
  --accent-gradient: linear-gradient(180deg, var(--gold) 0%, var(--gold-deep) 100%);

  /* Text drawn over a playing picture, where the surfaces do not apply.
     Three steps, matching the three text roles (DesignSystem.swift:44-46). */
  --on-film: rgba(255, 255, 255, 1);
  --on-film-secondary: rgba(255, 255, 255, 0.72);
  --on-film-faint: rgba(255, 255, 255, 0.45);

  /* ONE surface colour at two steps. A second surface colour "is how an
     interface starts looking like a settings screen" (DesignSystem.swift:20).
     Flat, not gradients, the indigo Cinema Glow is gone. */
  --surface: var(--surface-1);
  --surface-strong: var(--surface-2);
  --surface-flat: var(--surface-1);

  /* Hairlines, never borders. 8% white reads as a seam, not a box
     (DesignSystem.swift:37-38). There is deliberately no heavier variant:
     a heavier hairline is the beginning of a box. */
  --seam: rgba(255, 255, 255, 0.08);

  /* Type */
  --font-display: "SF Pro Display", -apple-system, BlinkMacSystemFont,
    "Helvetica Neue", "Segoe UI", Roboto, Arial, sans-serif;
  --font-body: "SF Pro Text", -apple-system, BlinkMacSystemFont,
    "Helvetica Neue", "Segoe UI", Roboto, Arial, sans-serif;
  --font-mono: "SF Mono", ui-monospace, SFMono-Regular, Menlo, Consolas,
    "Liberation Mono", monospace;

  /* Type scale, fluid. Minimums are the 320px values. */
  --text-2xs: 0.8125rem;                                    /* 13px floor */
  --text-xs: 0.875rem;                                      /* 14px */
  --text-sm: 0.9375rem;                                     /* 15px */
  --text-base: clamp(1rem, 0.965rem + 0.18vw, 1.0625rem);   /* 16 - 17 */
  --text-lg: clamp(1.0625rem, 1rem + 0.32vw, 1.1875rem);    /* 17 - 19 */
  --text-xl: clamp(1.1875rem, 1.09rem + 0.5vw, 1.4375rem);  /* 19 - 23 */
  --text-2xl: clamp(1.4375rem, 1.25rem + 0.95vw, 2rem);     /* 23 - 32 */
  --text-3xl: clamp(1.75rem, 1.4rem + 1.75vw, 2.75rem);     /* 28 - 44 */
  --text-display: clamp(2.5rem, 1.55rem + 4.4vw, 5rem);     /* 40 - 80 */

  /* Tracking. The brand asks for a neo-grotesque with sharp terminals;
     SF Pro Display is the sanctioned fallback and it needs negative
     tracking at display sizes to sit right. */
  --track-display: -0.035em;
  --track-title: -0.022em;
  --track-body: -0.005em;
  --track-wide: 0.08em;

  --leading-display: 0.98;
  --leading-title: 1.14;
  --leading-body: 1.65;

  /* Space scale */
  --space-2xs: 0.25rem;
  --space-xs: 0.5rem;
  --space-sm: 0.75rem;
  --space-md: 1rem;
  --space-lg: 1.5rem;
  --space-xl: 2rem;
  --space-2xl: 3rem;
  --space-3xl: clamp(3.5rem, 8vw, 6rem);
  --space-4xl: clamp(5rem, 11vw, 8.5rem);

  /* Layout */
  --container: 72rem;   /* 1152px */
  --measure: 68ch;      /* long-form line length */
  --gutter: clamp(1.25rem, 5vw, 2.5rem);

  /* Shape */
  --radius-sm: 8px;
  --radius: 14px;
  --radius-lg: 22px;
  --radius-pill: 999px;

  /* Elevation is a surface step plus a seam. The app draws two shadows in
     its entire codebase, so the web gets one: a soft bloom under the single
     primary action, in the accent's own colour. */
  --bloom: 0 14px 34px -16px rgba(242, 192, 120, 0.34);

  /* Motion */
  --ease: cubic-bezier(0.2, 0.7, 0.25, 1);
  --dur-fast: 140ms;
  --dur: 260ms;
  --dur-slow: 640ms;

  color-scheme: dark;
}

/* --------------------------------------------------------------------------
   2. RESET AND BASE
   Dark only. There is no light mode: the product is a black-ground cinema
   tool and a light variant would be a different brand.
   -------------------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  /* No horizontal scroll at 320px, whatever a section tries to do. */
  overflow-x: hidden;
  background-color: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-body);
  letter-spacing: var(--track-body);
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  /* Long unbroken strings (a URL in legal copy) wrap instead of overflowing. */
  overflow-wrap: break-word;
}

img,
svg,
video {
  display: block;
  max-width: 100%;
  height: auto;
}

a {
  color: inherit;
}

h1,
h2,
h3,
h4 {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: var(--track-title);
  line-height: var(--leading-title);
  margin: 0;
  text-wrap: balance;
}

p {
  margin: 0;
}

ul,
ol {
  margin: 0;
}

:focus-visible {
  outline: 2px solid var(--text);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}

::selection {
  background: rgba(242, 192, 120, 0.26);
  color: var(--text);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.center {
  text-align: center;
}

/* --------------------------------------------------------------------------
   3. SHELL, HEADER, FOOTER
   -------------------------------------------------------------------------- */

.page-shell {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
}

/* A content page's <main> needs no class of its own. */
.page-shell > main {
  flex: 1 0 auto;
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--space-3xl) var(--gutter) var(--space-4xl);
}

/* The landing page opts out so its sections can run edge to edge. */
.page-shell > main.main-flush {
  max-width: none;
  padding: 0;
}

.site-header {
  /* STAYS. The page is a single scroll-linked composition several thousand
     pixels long, and a nav that leaves at the top means the only way back to
     any section is to scroll the whole runway again. */
  position: sticky;
  top: 0;
  /* Above the rig, which carries z-index 1, and below the dot cursor at 9999. */
  z-index: 20;
  /* IT GETS OUT OF THE WAY. Hidden by a transform rather than by leaving the
     layout: the anchor-scroll measures this element's height to know how far to
     stop short, and a header that collapses would measure zero and park every
     section title back under the bar it was just fixed to clear.
     Transitioned, and that is allowed here for the reason the rest of this file
     is not: this is a STATE change, shown or hidden, not a position being
     interpolated from scroll. The driver's rule is about motion that competes
     with the reader's scrolling; this answers it. */
  transition: translate 260ms var(--ease), opacity 200ms var(--ease);
  will-change: translate;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--space-lg) var(--gutter);
  border-bottom: 1px solid var(--seam);
}

/* THE BAND BEHIND IT, FULL BLEED. The header itself is capped at the container
   width and centred, so its own background would stop where the text does and
   leave the film running past it on both sides. This paints the full viewport
   width behind it and nothing else, the blur is what keeps the nav legible
   over a moving picture without a solid bar closing the top of the page. */
.site-header::before {
  content: "";
  position: absolute;
  inset: 0;
  left: 50%;
  width: 100vw;
  transform: translateX(-50%);
  background: rgba(10, 10, 12, 0.62);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  border-bottom: 1px solid var(--seam);
  z-index: -1;
}
.site-header > * { position: relative; }

/* Away. -101% rather than -100% so the ::before band's own bottom border goes
   with it instead of leaving a hairline across the top of the page. */
.site-header[data-away] {
  translate: 0 -101%;
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  /* A bar that comes and goes as the page moves IS motion, whatever it is
     made of. Reduced motion keeps it where it is and never hides it, the
     script checks the same query and stops driving it. */
  .site-header { transition: none; }
  .site-header[data-away] { translate: none; opacity: 1; }
}

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: var(--track-title);
  color: var(--text);
  text-decoration: none;
}

/* The logo master is a mark on its own black field, so it is presented as a
   tile rather than knocked out. The amber core is the one place amber
   appears outside a primary action. */
.logo-mark {
  width: 34px;
  height: 34px;
  border-radius: 10px;
  box-shadow: 0 0 0 1px var(--seam);
  flex: none;
}

.site-header nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-lg);
  font-size: var(--text-sm);
}

.site-header nav a {
  color: var(--text-secondary);
  text-decoration: none;
  transition: color var(--dur) var(--ease);
}

.site-header nav a:hover,
.site-header nav a[aria-current="page"],
.site-header nav a[aria-current="true"] {
  color: var(--text);
}

/* WHERE YOU ARE. The rule is drawn rather than the link underlined, because
   text-decoration sits on the type's own baseline and collides with the
   descenders in "sentence", a rule of our own can be placed where it reads as
   a marker rather than as struck-through text.
   Scaled from the middle so it opens outward when a section takes over, and
   transitioned because this is a state change, not a position read from scroll. */
.site-header nav a {
  position: relative;
  padding-bottom: 4px;
}
.site-header nav a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1.5px;
  border-radius: 2px;
  background: var(--gold);
  scale: 0 1;
  transition: scale 260ms var(--ease);
}
.site-header nav a[aria-current="true"]::after { scale: 1 1; }

@media (prefers-reduced-motion: reduce) {
  .site-header nav a::after { transition: none; }
}

.site-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md) var(--space-xl);
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--space-xl) var(--gutter) var(--space-2xl);
  border-top: 1px solid var(--seam);
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

.site-footer p {
  margin: 0;
}

.site-footer nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md) var(--space-lg);
}

.site-footer a {
  color: var(--text-secondary);
  text-decoration: none;
  transition: color var(--dur) var(--ease);
}

.site-footer a:hover {
  color: var(--text);
}

/* --------------------------------------------------------------------------
   4. TITLES, META, UTILITIES
   -------------------------------------------------------------------------- */

.page-title {
  font-size: var(--text-3xl);
  letter-spacing: var(--track-display);
  margin-bottom: var(--space-sm);
}

.page-meta {
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--text-secondary);
  margin: 0 0 var(--space-2xl);
}

.eyebrow {
  font-size: var(--text-2xs);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  /* Gold. These are the only labels on the page that name what a section IS
     rather than say something, so they can carry the accent without competing
     with the headline underneath, at this size and tracking the colour reads
     as a marker, not as emphasis. Measured 11.9:1 on the app ground, which is
     past AAA for a size that only needs 4.5:1. */
  color: var(--gold);
  margin: 0 0 var(--space-md);
}

.lede {
  font-size: var(--text-lg);
  color: var(--text-secondary);
  max-width: 46ch;
  margin: 0;
}

.footnote {
  font-size: var(--text-xs);
  color: var(--text-secondary);
  margin: 0;
}

/* An unresolved fact, marked in the page rather than guessed at. */
.todo {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  line-height: 1.5;
  letter-spacing: 0;
  color: var(--text-secondary);
  background: rgba(243, 244, 246, 0.05);
  border: 1px dashed var(--seam);
  border-radius: var(--radius-sm);
  padding: var(--space-2xs) var(--space-xs);
}

.stack > * + * {
  margin-top: var(--space-md);
}

/* Spacing and colour utilities, so no page ever needs a style attribute. */
.muted {
  color: var(--text-secondary);
}

.mt-md {
  margin-top: var(--space-md);
}

.mt-lg {
  margin-top: var(--space-lg);
}

.mt-xl {
  margin-top: var(--space-xl);
}

/* --------------------------------------------------------------------------
   5. BUTTONS
   -------------------------------------------------------------------------- */

.btn,
.btn-amber {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  min-height: 48px;
  padding: 0.85rem 1.5rem;
  border-radius: var(--radius-pill);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: -0.01em;
  text-decoration: none;
  cursor: pointer;
  border: 1px solid var(--seam);
  background: transparent;
  color: var(--text);
  transition: border-color var(--dur) var(--ease),
    background-color var(--dur) var(--ease), transform var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease);
}

.btn:hover {
  border-color: var(--text);
  background-color: rgba(243, 244, 246, 0.06);
}

.btn:active,
.btn-amber:active {
  transform: translateY(1px);
}

/* THE reserved amber. One per page. */
.btn-amber {
  border-color: transparent;
  background-image: var(--accent-gradient);
  color: var(--ink);
  box-shadow: var(--bloom);
}

.btn-amber:hover {
  box-shadow: 0 20px 52px -16px rgba(242, 192, 120, 0.34);
}

/* --------------------------------------------------------------------------
   6. CARD
   -------------------------------------------------------------------------- */

.card {
  background-image: var(--surface);
  border: 1px solid var(--seam);
  border-radius: var(--radius-lg);
  padding: var(--space-xl);
  box-shadow: none;
}

.card > * + * {
  margin-top: var(--space-md);
}

.card h2,
.card h3 {
  font-size: var(--text-xl);
}

.card p {
  color: var(--text-secondary);
}

/* --------------------------------------------------------------------------
   7. PROSE, long-form legal and support copy
   Everything a privacy policy, a EULA or a support page can contain is
   styled here, so those pages need no CSS of their own.
   -------------------------------------------------------------------------- */

.prose {
  max-width: var(--measure);
  color: var(--text-secondary);
  font-size: var(--text-base);
  line-height: var(--leading-body);
}

.prose > * + * {
  margin-top: var(--space-md);
}

.prose h2 {
  font-size: var(--text-2xl);
  color: var(--text);
  margin-top: var(--space-2xl);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--seam);
}

.prose h3 {
  font-size: var(--text-xl);
  color: var(--text);
  margin-top: var(--space-xl);
}

.prose h4 {
  font-size: var(--text-lg);
  color: var(--text);
  margin-top: var(--space-lg);
}

.prose > :first-child {
  margin-top: 0;
}

.prose h2 + *,
.prose h3 + *,
.prose h4 + * {
  margin-top: var(--space-sm);
}

.prose ul,
.prose ol {
  padding-left: 1.35rem;
}

.prose li + li {
  margin-top: var(--space-xs);
}

.prose li > ul,
.prose li > ol {
  margin-top: var(--space-xs);
}

.prose li::marker {
  color: var(--text-faint);
}

.prose ol li::marker {
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

.prose a {
  color: var(--text);
  text-decoration: underline;
  text-decoration-color: var(--seam);
  text-underline-offset: 0.22em;
  text-decoration-thickness: 1px;
  transition: text-decoration-color var(--dur) var(--ease);
}

.prose a:hover {
  text-decoration-color: var(--text);
}

.prose strong {
  color: var(--text);
  font-weight: 600;
}

.prose em {
  font-style: italic;
}

.prose blockquote {
  margin: var(--space-lg) 0;
  padding-left: var(--space-lg);
  border-left: 2px solid var(--seam);
  color: var(--text-secondary);
}

.prose hr {
  border: 0;
  border-top: 1px solid var(--seam);
  margin: var(--space-2xl) 0;
}

.prose code {
  font-family: var(--font-mono);
  font-size: 0.9em;
  letter-spacing: 0;
  color: var(--text);
  background: rgba(243, 244, 246, 0.06);
  border: 1px solid var(--seam);
  border-radius: var(--radius-sm);
  padding: 0.1em 0.38em;
}

.prose pre {
  overflow-x: auto;
  background: var(--surface-flat);
  border: 1px solid var(--seam);
  border-radius: var(--radius);
  padding: var(--space-md);
  font-size: var(--text-xs);
}

.prose pre code {
  background: none;
  border: 0;
  padding: 0;
}

.prose dt {
  color: var(--text);
  font-weight: 600;
  margin-top: var(--space-md);
}

.prose dd {
  margin: var(--space-2xs) 0 0;
}

/* A wide table scrolls inside itself. `display: block` on the table is what
   makes that work without a wrapper element, which matters because the pages
   using .prose are written with these classes only. */
.prose table {
  display: block;
  max-width: 100%;
  overflow-x: auto;
  border-collapse: collapse;
  font-size: var(--text-sm);
  margin: var(--space-lg) 0;
}

.prose th,
.prose td {
  text-align: left;
  vertical-align: top;
  padding: var(--space-sm) var(--space-md) var(--space-sm) 0;
  border-bottom: 1px solid var(--seam);
}

.prose th {
  color: var(--text);
  font-weight: 600;
  font-size: var(--text-xs);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  white-space: nowrap;
}

.prose .card {
  color: var(--text-secondary);
}

/* --------------------------------------------------------------------------
   8. LANDING PAGE
   Everything below is used by web/index.html and web/404.html. It is here
   rather than in a <style> block so that this file stays the only stylesheet.
   -------------------------------------------------------------------------- */

.section {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--space-3xl) var(--gutter);
  /* THE STICKY HEADER OWNS THE TOP OF THE WINDOW, so a section scrolled to by
     its id has to stop below it or the browser parks its title underneath.
     Landing on /#faq put "Questions" behind the bar and opened on the first
     question, which reads as a page that starts halfway through itself.

     scroll-margin-top rather than an offset in the driver, because this is not
     only the driver's problem: a typed URL, a reload on a fragment, and the
     back button all scroll natively and none of them go through site.js. One
     property fixes every path at once.

     Deliberately NOT on .beat. Beats are also anchor targets, but they snap on
     `center` and scroll-margin moves the snap position too, a beat is a full
     viewport with its copy centred, so nothing is under the bar there anyway. */
  scroll-margin-top: 7.5rem;
}

/* Landing sections each carry top and bottom padding so any one of them can
   stand alone. Adjacent ones would otherwise stack both, so every section
   after the first drops its top padding and the previous section's bottom
   padding becomes the single gap. */
.main-flush > section + section {
  padding-top: 0;
}

.section-head {
  max-width: 52ch;
  margin-bottom: var(--space-2xl);
}

.section-head h2 {
  font-size: var(--text-3xl);
  letter-spacing: var(--track-display);
}

.section-head p {
  margin-top: var(--space-md);
  color: var(--text-secondary);
  font-size: var(--text-lg);
}

/* --- Hero --- */

.hero {
  position: relative;
  isolation: isolate;
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--space-4xl) var(--gutter) var(--space-3xl);
}

/* The ambient amber, tied to the mark. Kept under 10% so it reads as light
   in a black room rather than as a coloured background. */
.hero::before {
  content: "";
  position: absolute;
  z-index: -1;
  top: -12%;
  left: 50%;
  /* 92% leaves room for the 1.04 scale in `breathe`, so the glow never
     reaches the viewport edge and scrollWidth never exceeds clientWidth
     at 320px. */
  width: min(46rem, 92%);
  aspect-ratio: 1;
  translate: -50% 0;
  background: radial-gradient(
    circle,
    rgba(242, 192, 120, 0.09) 0%,
    rgba(217, 154, 72, 0.05) 34%,
    transparent 66%
  );
  animation: breathe 9s var(--ease) infinite alternate;
  pointer-events: none;
}

.hero-mark {
  width: 84px;
  height: 84px;
  border-radius: 22px;
  box-shadow: 0 0 0 1px var(--seam), 0 30px 70px -30px rgba(242, 192, 120, 0.30);
  margin-bottom: var(--space-xl);
}

.hero h1 {
  font-size: var(--text-display);
  line-height: var(--leading-display);
  letter-spacing: var(--track-display);
  max-width: 15ch;
  margin-bottom: var(--space-lg);
}

.hero .lede {
  font-size: var(--text-xl);
  max-width: 42ch;
}

.hero-actions {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-md);
  margin-top: var(--space-xl);
}

/* App Store badge stand-in. Deliberately not a facsimile of Apple's badge:
   it is a slot, and it says so. */
.badge-slot {
  display: inline-flex;
  flex-direction: column;
  gap: var(--space-2xs);
  justify-content: center;
  min-height: 48px;
  max-width: 44ch;
  padding: var(--space-sm) var(--space-md);
  border: 1px dashed var(--seam);
  border-radius: var(--radius);
  font-size: var(--text-2xs);
  line-height: 1.4;
  color: var(--text-secondary);
}

.badge-slot strong {
  color: var(--text);
  font-weight: 600;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
}

/* --- The one question --- */

.question {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--space-3xl) var(--gutter);
}

.question-frame {
  background-image: var(--surface-strong);
  border: 1px solid var(--seam);
  border-radius: var(--radius-lg);
  padding: clamp(var(--space-xl), 6vw, var(--space-3xl));
  box-shadow: none;
}

.question-ask {
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  font-weight: 600;
  letter-spacing: var(--track-display);
  line-height: var(--leading-title);
  margin: 0;
}

.question-answer {
  font-size: var(--text-lg);
  color: var(--text-secondary);
  margin: var(--space-md) 0 0;
  max-width: 54ch;
}

.defaults {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin: var(--space-xl) 0 0;
  padding: 0;
  list-style: none;
}

.defaults li {
  font-size: var(--text-2xs);
  letter-spacing: 0.02em;
  color: var(--text-secondary);
  background: rgba(243, 244, 246, 0.05);
  border: 1px solid var(--seam);
  border-radius: var(--radius-pill);
  padding: var(--space-2xs) var(--space-sm);
}

/* --- Grids: steps and cards --- */

.grid {
  display: grid;
  gap: var(--space-lg);
  grid-template-columns: 1fr;
}

@media (min-width: 46rem) {
  .grid-2 {
    grid-template-columns: repeat(2, 1fr);
  }

  .grid-3 {
    grid-template-columns: repeat(3, 1fr);
  }
}

@media (min-width: 62rem) {
  .grid-4 {
    grid-template-columns: repeat(4, 1fr);
  }
}

@media (min-width: 46rem) and (max-width: 61.99rem) {
  .grid-4 {
    grid-template-columns: repeat(2, 1fr);
  }
}

.step {
  border-top: 1px solid var(--seam);
  padding-top: var(--space-lg);
}

.step-index {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--track-wide);
  color: var(--text-secondary);
  display: block;
  margin-bottom: var(--space-sm);
}

.step h3 {
  font-size: var(--text-xl);
  margin-bottom: var(--space-sm);
}

.step p {
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

/* --- Lumens --- */

.lumens {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--space-3xl) var(--gutter);
}

.lumens-frame {
  position: relative;
  overflow: hidden;
  background-image: var(--surface);
  border: 1px solid var(--seam);
  border-radius: var(--radius-lg);
  padding: clamp(var(--space-xl), 6vw, var(--space-3xl));
}

/* The Lumens accent: the second sanctioned use of amber. */
.lumens-frame::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 3px;
  background-image: var(--accent-gradient);
}

.lumens-frame h2 {
  font-size: var(--text-3xl);
  letter-spacing: var(--track-display);
}

.lumens-frame .lede {
  margin-top: var(--space-md);
  max-width: 56ch;
}

.lumens-body {
  margin-top: var(--space-xl);
  max-width: 60ch;
  color: var(--text-secondary);
}

.lumens-count {
  color: var(--accent);
  font-family: var(--font-display);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

.packs {
  display: grid;
  gap: var(--space-md);
  grid-template-columns: 1fr;
  margin: var(--space-xl) 0 0;
  padding: 0;
  list-style: none;
}

@media (min-width: 46rem) {
  .packs {
    grid-template-columns: repeat(3, 1fr);
  }
}

.packs li {
  border: 1px solid var(--seam);
  border-radius: var(--radius);
  padding: var(--space-md);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.packs .pack-name {
  display: block;
  color: var(--text);
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: var(--track-title);
  margin-bottom: var(--space-2xs);
}

/* --- FAQ --- */

.faq {
  border-top: 1px solid var(--seam);
}

.faq details {
  border-bottom: 1px solid var(--seam);
}

.faq summary {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-lg) 0;
  cursor: pointer;
  list-style: none;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: var(--track-title);
  color: var(--text);
  transition: color var(--dur) var(--ease);
}

.faq summary::-webkit-details-marker {
  display: none;
}

.faq summary::after {
  content: "+";
  font-family: var(--font-mono);
  font-weight: 400;
  color: var(--text-secondary);
  flex: none;
  transition: transform var(--dur) var(--ease);
}

.faq details[open] summary::after {
  transform: rotate(45deg);
}

.faq summary:hover {
  color: var(--text-secondary);
}

.faq-answer {
  padding: 0 0 var(--space-lg);
  color: var(--text-secondary);
  max-width: var(--measure);
}

.faq-answer > * + * {
  margin-top: var(--space-sm);
}

/* --- 404 --- */

.notfound {
  min-height: 58vh;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: var(--space-lg);
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding: var(--space-4xl) var(--gutter);
}

.notfound h1 {
  font-size: var(--text-3xl);
  letter-spacing: var(--track-display);
}

/* --------------------------------------------------------------------------
   9. MOTION
   -------------------------------------------------------------------------- */

@keyframes breathe {
  from {
    opacity: 0.55;
    scale: 0.94;
  }
  to {
    opacity: 1;
    scale: 1.04;
  }
}

@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.rise {
  animation: rise var(--dur-slow) var(--ease) both;
}

.rise-2 {
  animation-delay: 90ms;
}

.rise-3 {
  animation-delay: 180ms;
}

.rise-4 {
  animation-delay: 270ms;
}

/* Every animation and transition in this file is cancelled here. Nothing
   below the fold depends on an animation to become visible, so reduced
   motion loses nothing but the movement. */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    animation-delay: 0ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}


/* ==========================================================================
   14. THE DYNAMIC LAYER, hero reel, the phone, scroll choreography
   Added for the v2 rework. Everything here is PROGRESSIVE ENHANCEMENT: the
   page is complete and correct with this layer inert. The phone simply sits
   still, the reel shows its first frame, and every section is readable.

   Classes added, for the contract at the top of this file:
     .reel .reel-frame .reel-scrim        the full-bleed crossfading hero
     .rig .rig-sticky .rig-beats          one sticky phone, copy scrolling through
     .phone .phone-body .phone-screen     the CSS-3D device
     .phone-notch .phone-rail .phone-spec device detail
     .scr .scr-* (one per section)        the screens inside the phone
     .beat .beat-head .beat-body          a section of copy beside the phone
   -------------------------------------------------------------------------- */

/* --- 14.1 THE RIG, one phone, from viewport to device ------------------
   There is exactly one phone on this page and it never stops existing. It
   begins the size of the viewport, with no bezel and no corner radius, so what
   you see is a full-bleed video. As you scroll it SHRINKS into a device: the
   box narrows to the screen rect, the corners round, the bezel grows from
   nothing, and the picture inside is carried along by object-fit rather than
   being re-cropped.

   The earlier build had two phones, one for the reveal, one for the sections
  , which meant a handoff, and a handoff is a seam. Interpolating one object's
   box removes the question entirely: there is nothing to hand off to.

   Everything reads --p, 0 at full bleed and 1 at landed.
   ------------------------------------------------------------------------ */

.rig {
  position: relative;
  --screen-w: 300px;
  --screen-h: 620px;
  --screen-r: 41px;
  --bezel: 3px;
  --land-x: 0px;
}

@media (min-width: 62rem) {
  .rig {
    --screen-w: 330px;
    --screen-h: 682px;
    /* A QUARTER of the container, so the phone sits centred in one half and
       the copy gets the other half whole. A constant rather than
       min(25vw, ...): the container is a fixed 72rem, so the quarter is a
       fixed 288px, and a min() nested inside the drift's multiplication chain
       silently evaluated the whole translate to zero. */
    --land-x: 288px;
  }
}

/* The runway, and the three phases that share it. They run in sequence rather
   than overlapping, because the copy arriving while the phone is still large
   and centred is how the two ended up on top of each other:

     0.00 - 0.70   SHRINK   viewport-sized picture becomes a device, centred
     0.70 - 0.90   DRIFT    the device travels to its bay
     0.90 - 1.00   COPY     only now does the text arrive

   Each phase remaps --p into its own 0-1 with a clamp, so no phase can start
   before the one before it has finished. */
.rig-runway { height: 300svh; }

.rig {
  --p-shrink: clamp(0, calc(var(--p, 0) / 0.7), 1);
  --p-drift:  clamp(0, calc((var(--p, 0) - 0.7) * 5), 1);
  --p-copy:   clamp(0, calc((var(--p, 0) - 0.9) * 10), 1);
}

.rig-sticky {
  position: sticky;
  top: 0;
  height: 100svh;
  display: grid;
  place-items: center;
  overflow: clip;
  z-index: 1;
  pointer-events: none;
}

/* The copy scrolls THROUGH the sticky stage rather than after it. */
.rig-beats {
  margin-top: -100svh;
  position: relative;
  z-index: 2;
  /* Held back until the phone is settled in its bay. Without this the copy
     reveals on its own observer, at full opacity, while the phone is still
     viewport-sized and centred, which is exactly how they overlap. */
  opacity: var(--p-copy);
}

.phone-rig {
  perspective: 1600px;
  perspective-origin: 50% 45%;
  /* The landing drift, across the last third, once the box has nearly
     finished shrinking. Moving earlier reads as the page sliding. */
  /* --drift-x is written in pixels by site.js. Four different pure-CSS
     spellings of this multiplication all computed to 0px in Chromium, a
     length-clamp in a custom property, a three-operand chain, a min() inside
     the product, and the exact two-operand form the shrink uses successfully.
     The driver already writes --p every frame, so writing a resolved pixel
     value beside it costs nothing and cannot be argued with. */
  /* Also scroll-linked; a transition here would fight the frame-by-frame
     write and lag the phone behind its own position. */
  /* --lift-y lets a beat sit high or low in the bay, so a big shot can be
     nudged up off the copy rather than colliding with it. Both are written in
     pixels by site.js and both fall back to zero, which is the no-JS rest. */
  translate: var(--drift-x, 0px) var(--lift-y, 0px);
}

/* Direction flips via the sign of --drift-x, applied in site.js. */

.phone {
  /* THE INTERPOLATION. At p=0 the phone is the viewport; at p=1 it is the
     device. Nothing is scaled, so the screen's contents never distort. */
  width: calc(100vw - var(--p-shrink) * (100vw - var(--screen-w)));
  height: calc(100svh - var(--p-shrink) * (100svh - var(--screen-h)));
  position: relative;
  transform-style: preserve-3d;
  pointer-events: auto;

  /* SCROLL-LINKED, NOT EVENT-DRIVEN. There is deliberately no transition here.
     The angle was previously set per beat and animated over a fixed 900ms
     (1750ms for the full turn), which meant the motion had its own clock: a
     reader scrolling faster than that clock retargeted the transition
     mid-flight and the 384-degree turn never completed, while a slow reader
     watched it finish and then sit still. Both are the same bug, time was
     driving the phone instead of scroll.

     --rot and --tilt are written every frame by site.js as a pure function of
     scroll position, exactly as the shrink already was. The turn therefore
     always completes, at whatever speed it is scrolled, and reverses honestly
     when scrolled back. */
  /* scale is LAST in the list and therefore applied FIRST: the phone is sized,
     then the sized phone is rotated. Written the other way round the rotation
     happens in unscaled space and the perspective foreshortening no longer
     matches the object you are looking at.

     A uniform scale is safe here in a way it was not for the runway. The
     runway interpolates width and height because it travels from the viewport's
     aspect to the device's, and scaling across that would stretch the picture.
     Scaling a phone that is already the right shape distorts nothing. */
  /* The grab's two angles come FIRST, which puts them outside the pose, the
     reader's nudge happens in the view's frame rather than the device's, so it
     reads as turning the object rather than as editing the shot. device3d.js
     adds the same two numbers to the mesh; this line is the no-WebGL floor. */
  transform: rotateY(var(--grab-rot, 0deg)) rotateX(var(--grab-tilt, 0deg))
             rotateY(var(--idle-rot, 0deg)) rotateX(var(--idle-tilt, 0deg))
             rotateZ(var(--roll, 0deg)) rotateY(var(--rot, 0deg)) rotateX(var(--tilt, 0deg)) scale(var(--phone-scale, 1));
}

.phone-face {
  position: absolute;
  inset: 0;
  border-radius: calc(var(--p-shrink) * var(--screen-r));
  backface-visibility: hidden;
  overflow: hidden;
}
.phone-face-front { transform: translateZ(5px); }

.phone-face-back {
  transform: rotateY(180deg) translateZ(5px);
  background: linear-gradient(155deg, #232329 0%, #131317 42%, #0d0d10 100%);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.05);
  opacity: var(--p-shrink);
}
/* The camera plateau. A raised square with two lenses, because the back is
   fully in view for the whole of the introduction turn and a blank panel is
   the moment the illusion drops. Lenses are drawn with layered radial
   gradients: a dark barrel, a glass dome, and one small specular dot that sits
   off-centre so the two lenses do not look stamped from the same die. */
.phone-face-back::after {
  content: "";
  position: absolute;
  top: 16px; left: 16px;
  width: 92px; height: 92px;
  border-radius: 26px;
  background:
    radial-gradient(circle at 33% 32%, #0b0b0e 0 15px, transparent 15px),
    radial-gradient(circle at 30% 29%, rgba(120,150,190,0.5) 0 4px, transparent 5px),
    radial-gradient(circle at 67% 66%, #0b0b0e 0 15px, transparent 15px),
    radial-gradient(circle at 64% 63%, rgba(120,150,190,0.4) 0 4px, transparent 5px),
    linear-gradient(150deg, #2f2f37, #17171b);
  box-shadow:
    inset 0 0 0 1px rgba(255,255,255,0.07),
    0 2px 6px rgba(0,0,0,0.5);
}

/* THE RAILS, all four of them, which is the point.
   This was a four-face box: front, back, left, right, and nothing capping the
   top or the bottom. Those two faces are exactly the ones `tilt` reveals, so
   leaning the phone looked straight into a hole where a rail should be, and
   beat two leans 45 degrees. More inclination made it look MORE like a
   blockout, because the geometry could not pay for the angle it was being
   asked to hold.

   Six faces now, which is the standard construction for a phone in CSS: a box
   whose sides are each rotated a quarter turn out of the plane and pushed half
   the thickness along their own Z, so they meet at the edges of the front and
   back. The joins are mitred rather than truly curved, CSS cannot round a
   corner across two 3D planes, so the rail material is kept dark and the
   chamfer highlights stop short of the ends, which is what stops the corner
   reading as a seam. */
.phone-edge {
  position: absolute;
  opacity: var(--p-shrink);
  /* Machined metal rather than flat grey: a dark core with a bright chamfer
     down each long side, which is what actually reads as a milled rail when
     the phone turns and the highlight travels along it. */
  background:
    linear-gradient(var(--rail-angle, 180deg),
      #4a4a55 0%, #2a2a31 8%, #17171b 26%, #131317 50%,
      #17171b 74%, #2a2a31 92%, #4a4a55 100%);
}
/* The two long rails carry the buttons, so they need a stacking context. */
/* Inset by the corner radius, because a straight rail run to the full height
   sticks out past the rounded corners of the front and back faces and breaks
   the silhouette, the one thing a device mockup cannot afford. Stopping where
   the curve begins leaves a small dark notch at each corner instead, which is
   the honest compromise: CSS cannot round a surface across two 3D planes, so
   the choice is a gap or an overhang, and a gap in shadow reads as a chamfer
   while an overhang reads as a mistake. Ends are rounded to soften the join. */
.phone-edge-l, .phone-edge-r {
  top: calc(var(--p-shrink) * var(--screen-r));
  bottom: calc(var(--p-shrink) * var(--screen-r));
  width: 10px;
  border-radius: 3px;
  --rail-angle: 90deg;
  transform-style: preserve-3d;
}
.phone-edge-l { left: 0;  transform: rotateY(-90deg) translateZ(5px); transform-origin: left center; }
.phone-edge-r { right: 0; transform: rotateY(90deg)  translateZ(5px); transform-origin: right center; }

/* The two that were missing. Same construction rotated onto the other axis. */
.phone-edge-t, .phone-edge-b {
  left: calc(var(--p-shrink) * var(--screen-r));
  right: calc(var(--p-shrink) * var(--screen-r));
  height: 10px;
  border-radius: 3px;
  --rail-angle: 180deg;
}
.phone-edge-t { top: 0;    transform: rotateX(90deg)  translateZ(5px); transform-origin: center top; }
.phone-edge-b { bottom: 0; transform: rotateX(-90deg) translateZ(5px); transform-origin: center bottom; }

/* The buttons sit ON the rails, as children, so they inherit the rail's place
   in 3D and travel with it instead of being flat stickers on the front. */
.phone-btn {
  position: absolute;
  left: 1px; right: 1px;
  border-radius: 2px;
  background: linear-gradient(90deg, #56565f, #34343c 40%, #2a2a31);
  box-shadow: 0 0 0 0.5px rgba(0,0,0,0.55);
}
.phone-btn-action { top: 108px; height: 28px; }
.phone-btn-vol-up { top: 158px; height: 44px; }
.phone-btn-vol-dn { top: 214px; height: 44px; }
.phone-btn-power  { top: 172px; height: 66px; }

/* The bezel grows out of nothing rather than fading in over the picture. */
.phone-body {
  position: absolute;
  inset: 0;
  border-radius: calc(var(--p-shrink) * var(--screen-r));
  border: calc(var(--p-shrink) * var(--bezel)) solid #24242a;
  box-shadow:
    0 calc(var(--p-shrink) * 40px) calc(var(--p-shrink) * 90px) calc(var(--p-shrink) * -40px) rgba(0,0,0,0.92),
    inset 0 0 0 1px rgba(255,255,255, calc(var(--p-shrink) * 0.06));
  pointer-events: none;
  z-index: 3;
}

.phone-spec {
  position: absolute;
  inset: 0;
  border-radius: calc(var(--p-shrink) * var(--screen-r));
  background: linear-gradient(115deg,
    rgba(255,255,255,0) 34%, rgba(255,255,255,0.10) 46%,
    rgba(255,255,255,0.02) 53%, rgba(255,255,255,0) 64%);
  opacity: var(--p-shrink);
  pointer-events: none;
  z-index: 4;
}

.phone-notch {
  position: absolute;
  top: 12px; left: 50%;
  translate: -50% 0;
  width: 84px; height: 24px;
  border-radius: 999px;
  background: #08080a;
  opacity: clamp(0, calc((var(--p-shrink) - 0.5) * 2), 1);
  z-index: 4;
}

.phone-screen {
  position: absolute;
  inset: calc(var(--p-shrink) * var(--bezel));
  border-radius: calc(var(--p-shrink) * (var(--screen-r) - var(--bezel)));
  overflow: hidden;
  background: var(--ink);
  display: grid;
}

/* --- 14.2 The reel, which is simply the first screen --------------------- */

.scr.scr-reel {
  /* Doubled class on purpose. .scr sets the app-screen padding and is declared
     later in this file, so at equal specificity it wins on source order and
     the video comes back inset by 42px inside its own screen. */
  padding: 0;
  /* AND display, for the same reason of source order. `.scr` is
     `display: flex; flex-direction: column`, and `grid-area`, which all three
     clips set to `1 / 1` precisely so they occupy one cell and stack, means
     nothing in a flex container. So they did not stack. They tiled down the
     screen as three flex items, each shrunk to a third of its height, and
     because only the active one carries `data-on` the hero showed a single
     horizontal band of video with the black phone-screen background above and
     below it. Read as "the video is black", which is what it was reported as
     twice. */
  display: grid;
}
.scr-reel video {
  grid-area: 1 / 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 700ms linear;   /* the app's 0.7s crossfade */

  /* The phone turns; the film must not. On the panoramic beat the device lies
     down a quarter turn and this undoes exactly that angle, so the picture
     stays upright while the frame around it goes wide. --reel-cover makes up
     the difference in area, because a box rotated inside its own footprint
     stops covering it. Both are written by site.js and both rest at their
     identity values, so on every beat that is not turned this is inert.

     The standalone `rotate` and `scale` properties rather than `transform`, so
     this composes with whatever the ancestor's transform is doing without
     having to restate it. */
  rotate: var(--reel-roll, 0deg);
  scale: var(--reel-cover, 1);
}
.scr-reel video[data-on] { opacity: 1; }

/* Only while the picture is still the page. Once it is a screen, a scrim
   would read as dirt on the glass. */
.rig-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(10,10,12,0.92) 0%, rgba(10,10,12,0.30) 46%, rgba(10,10,12,0.55) 100%);
  opacity: calc(1 - var(--p-shrink));
  pointer-events: none;
  z-index: 2;
}

.rig-copy {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 0 var(--gutter) calc(var(--space-3xl) + 2.5rem);
  max-width: var(--container);
  margin-inline: auto;
  /* Was clamp(0, calc(1 - var(--p-shrink) * 2.6), 1), the same fade, computed
     in CSS where only CSS could see the answer. site.js writes it now, on the
     same --copy-o every beat uses, because the background field has to know how
     visible this copy is and could not read a calc(). One fade, one variable,
     one place that decides. */
  opacity: var(--copy-o, 1);
  pointer-events: auto;
  z-index: 5;
}

.reel-dots {
  /* CENTRED ON THE PAGE, still inside the copy block rather than pinned to the
     viewport bottom where a short window pushes them off screen entirely and
     nobody learns there are three looks. They report which film is playing, and
     the film is the whole window, so they belong on its centre line, not
     tucked against the left edge beside the button. */
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 6px;
  margin-top: var(--space-md);
  pointer-events: auto;
}
.reel-dot {
  position: relative;
  overflow: hidden;             /* clips the fill to the capsule */
  width: 6px; height: 6px;
  border-radius: 999px;
  border: 0; padding: 0;
  background: rgba(255, 255, 255, 0.35);
  transition: width 260ms var(--ease);
  cursor: pointer;
}
.reel-dot[aria-current="true"] { width: 22px; }

/* THE DWELL, DRAWN. The capsule fills over the five seconds the current film
   has, then empties as the next one takes over, the familiar story-bar, and
   the only thing on the page that tells a reader the reel is on a clock rather
   than waiting for them.
   scaleX from the left, so it composites and never lays anything out. The width
   is 22px, so a transform is the difference between one paint and sixty. */
.reel-dot::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--accent);
  transform: scaleX(var(--fill, 0));
  transform-origin: left center;
}
.reel-dot:focus-visible { outline: 2px solid var(--text); outline-offset: 3px; }

/* A QUOTED SENTENCE. The marks are in the markup rather than generated, so
   they are part of the text a screen reader announces and part of what is
   copied, this line IS a quotation of something someone said, not decoration
   around a heading.
   Hanging punctuation pulls the opening mark into the margin so the words below
   it still line up on the left; without it the first line starts a quarter of a
   character further in than every line under it, which reads as a mistake. */
.beat-quote {
  hanging-punctuation: first last;
}

/* --- 14.3 The beats -----------------------------------------------------

   SNAPPING. Each beat is a snap point, so the page comes to rest with one
   section composed rather than halfway between two, and a further flick is
   needed to move on.

   PROXIMITY, not mandatory: the runway above is one long scroll-linked
   animation, and a mandatory container would fight the reader for control of
   it the whole way down. Proximity snaps when they stop near a beat and stays
   out of the way otherwise. The runway is explicitly excluded, nothing there
   should ever be snapped to, because every position in it is a valid frame of
   the animation.

   scroll-snap-stop: always is the part that makes a beat impossible to skip
   past in one throw, which is the behaviour asked for. Swap `proximity` for
   `mandatory` below if it should be firmer still. */

html {
  scroll-snap-type: y proximity;
}

.rig-runway,
.rig-sticky {
  scroll-snap-align: none;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-snap-type: none; }
}

/* --- The beats ----------------------------------------------------------- */

.beat {
  min-height: 100svh;
  scroll-snap-align: center;
  scroll-snap-stop: always;
  display: grid;
  align-content: center;
  padding-inline: var(--gutter);
  max-width: var(--container);
  margin-inline: auto;
}

@media (min-width: 62rem) {
  /* Two halves. The copy takes the one the phone is not parked in. An earlier
     version used three columns keyed off a --lane token that was never
     defined, so it silently fell back to 420px while the phone was centred on
     the viewport rather than in that lane, which is how the two came to
     overlap. */
  .beat { grid-template-columns: 1fr 1fr; column-gap: var(--space-3xl); }
  .beat > * { grid-column: 1; }
  .beat[data-side="left"] > * { grid-column: 2; }

  /* STACKED. The two-column beat exists so the copy can sit beside a phone
     that has drifted out of its way. A beat whose phone is centred has no
     spare column, so the copy goes underneath instead: one column, centred,
     and parked in the lower third by align-content, leaving the upper half to
     the device. data-stack pairs with side: 0 in the LAYOUT table, set one
     without the other and the copy and the phone occupy the same pixels. */
  .beat[data-stack] {
    grid-template-columns: 1fr;
    justify-items: center;
    text-align: center;
    align-content: end;
    padding-bottom: var(--space-3xl);
  }
  .beat[data-stack] > * { grid-column: 1; }
  /* Wider than the 46ch column measure, because centred text reads worse in a
     narrow ragged column, but still short of the full container. */
  .beat[data-stack] .beat-body { max-width: 54ch; }
}

.beat-head { margin: 0 0 var(--space-md); }
.beat-body { color: var(--text-secondary); max-width: 46ch; }
.beat-body + .beat-body { margin-top: var(--space-md); }



/* --- 14.4 The screens inside it ----------------------------------------- */

/* Every screen is in the DOM at once. The active one is revealed. That is
   why the text on the phone is real, selectable, translatable text. */
.scr {
  grid-area: 1 / 1;
  padding: 42px 18px 18px;
  opacity: 0;
  transition: opacity 420ms var(--ease);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  pointer-events: none;
}
.scr[data-active] { opacity: 1; pointer-events: auto; }

/* A SCREEN THAT IS A REAL CAPTURE. The recording already contains the app's own
   status bar, padding and type, so this page must add none of its own: the 42px
   top padding every other .scr carries would push the app's status bar down the
   glass and leave a band of nothing above it. Full bleed, and the display's own
   mask does the rounding, the same silhouette the film uses. */
.scr-capture { padding: 0; }
.scr-shot {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  /* The capture is the device's own aspect, so cover crops nothing in practice;
     it is here so a re-record at a different size cannot letterbox. */
}

/* With JS inert nothing sets data-active, so the first screen must still
   show. This is the no-JS floor. */
.no-js .scr:first-of-type,
.scr:first-of-type:only-of-type { opacity: 1; }

.scr-eyebrow {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.092em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin: 0 0 10px;
}

.scr-ask {
  font-size: 25px;
  line-height: 1.16;
  letter-spacing: -0.02em;
  font-weight: 600;
  color: var(--text);
  margin: 0 0 14px;
}

.scr-sentence {
  font-size: 15px;
  line-height: 1.5;
  color: var(--text);
  margin: 0 0 auto;
}

.scr-offer {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  align-self: flex-start;
  padding: 8px 13px;
  border-radius: 999px;
  border: 1px solid var(--seam);
  font-size: 12.5px;
  color: var(--text-secondary);
  margin-top: 12px;
}
.scr-offer b { color: var(--accent); font-weight: 500; }

.scr-cta {
  margin-top: 14px;
  padding: 13px;
  border-radius: 15px;
  background: var(--accent-gradient);
  color: var(--ink);
  font-weight: 600;
  font-size: 14.5px;
  text-align: center;
}

/* A row that reports an answered decision. Leading rule, never a box. */
.scr-row {
  border-left: 2px solid var(--seam);
  padding: 7px 0 7px 11px;
  margin-bottom: 9px;
}
.scr-row b { display: block; font-size: 13.5px; color: var(--text); font-weight: 550; }
.scr-row span { font-size: 12px; color: var(--text-secondary); }
.scr-row-live { border-left-color: var(--accent); }

.scr-media {
  width: 100%;
  flex: 1;
  min-height: 0;
  border-radius: 15px;
  object-fit: cover;
  background: var(--surface);
}

.scr-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  flex: 1;
  min-height: 0;
}
.scr-tile {
  border-radius: 12px;
  background: var(--surface) center/cover;
  position: relative;
  overflow: hidden;
}
.scr-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Progress. The third and last sanctioned use of the accent. */
.scr-progress {
  height: 3px;
  border-radius: 3px;
  background: rgba(255,255,255,0.09);
  overflow: hidden;
  margin-top: 12px;
}
.scr-progress i {
  display: block;
  height: 100%;
  width: 62%;
  border-radius: 3px;
  background: var(--accent-gradient);
}

/* --- 14.5 Reveal on enter ----------------------------------------------- */

/* Copy is posed from scroll position, not transitioned to a pose.
   site.js writes --copy-o and --copy-y on each beat every frame from that
   beat's distance to the centre of the viewport, so the text fades out as it
   leaves and in as it arrives, at the reader's speed, and unwinds exactly on
   the way back up.

   There is deliberately NO transition here. There used to be a 700ms one,
   paired with a data-seen attribute that was set once and never removed , 
   which is why copy faded up on arrival and then stayed lit permanently. A
   transition on top of a per-frame value would also re-introduce the second
   clock this page has already removed twice: it would chase every frame's
   value rather than land on it, lagging the scroll and never settling.

   The fallbacks are the floor. No JS means no --copy-o, which means opacity 1
   and no offset, and every block is simply visible. */
.js .beat .beat-head,
.js .beat .beat-body {
  opacity: var(--copy-o, 1);
  transform: translateY(var(--copy-y, 0px));
  will-change: opacity, transform;
}

/* --- 14.6 Motion off ---------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .reel-frame {
    animation: none;
    transform: none;
  }
  /* Without the cycle, the first look must be the one you see. */
  .reel-frame:nth-child(1) { opacity: 1; }
  .phone { transform: none; }
  .scr { transition: none; }
  /* The driver returns before it ever paints, so --copy-o is never set and
     these would already fall back to lit. Stated anyway, so the floor does not
     depend on reading the JavaScript to know it holds. */
  .js .beat .beat-head,
  .js .beat .beat-body { opacity: 1; transform: none; will-change: auto; }
}

/* --- 14.6b The 3D device takes over ------------------------------------- */

/* Set by device3d.js only once a model has actually loaded. Until then, and
   forever, if WebGL is missing, the model 404s, or the reader asked for reduced
   motion, none of this applies and the CSS device is the device. That is why
   the CSS one is still here and still maintained: it is the floor, not a
   leftover. */
.device-3d .phone-face,
.device-3d .phone-edge,
.device-3d .phone-body,
.device-3d .phone-spec,
.device-3d .phone-notch {
  display: none;
}

/* The phone box still exists and is still sized by the runway, because
   device3d.js measures it to clamp a rolled shot to the viewport. It just
   stops painting itself. */
.device-3d .phone { background: none; }

/* The renderers mount here and cover it. Already sticky and therefore
   positioned, so the absolutely positioned layers anchor to it correctly. */
.device-3d .rig-sticky { position: sticky; }

/* CSS3DRenderer owns the screen's placement, so the absolute inset that pinned
   it inside the CSS bezel has to go or it fights the 3D transform. Size and
   corner radius come from the MODEL, set by device3d.js.

   TRANSPARENT, and this is the part that matters: the film is a texture on the
   display mesh now, and this element sits in front of it. Painting the ink
   background here would cover the film completely, a screen that drew and was
   hidden, which is indistinguishable from a screen that never drew and cost a
   long time to tell apart once already. The text screens paint their own
   background instead, below. */
.device-3d .phone-screen {
  position: relative;
  inset: auto;
  background: transparent;
}

/* The reel is on the mesh, so the DOM copy must not be seen, but it must stay
   RENDERED. display:none stops a video decoding, readyState never climbs, and
   the canvas has nothing to sample: the film disappears from the mesh AND the
   DOM at once. That mistake took the site down once. Alive and invisible is not
   the same as hidden. Doubled class to beat .scr[data-active]. */
.device-3d .scr.scr-reel {
  opacity: 0;
  pointer-events: none;
}

/* The text screens own the display while they are up, so they carry the ink
   themselves rather than relying on the container. */
.device-3d .scr:not(.scr-reel)[data-active] {
  background: var(--ink);
  border-radius: inherit;
}

/* THE DYNAMIC ISLAND, for the text screens only.
   The display mesh has a real cutout, so the film needs nothing drawn over it , 
   but a DOM text screen sitting in front of that cutout would paint straight
   across it. Proportions of the screen rather than pixels, so it survives the
   element being sized from whatever model is loaded. */
.device-3d .scr:not(.scr-reel)[data-active]::after {
  content: "";
  position: absolute;
  top: 1.6%;
  left: 50%;
  translate: -50% 0;
  width: 28%;
  height: 3.9%;
  border-radius: 999px;
  background: #000;
  z-index: 6;
  pointer-events: none;
}

/* --- 14.6d The status field, as the page's ground ----------------------- */

/* A port of the app's render-state graphic (StatusMatrix.metal). Created by
   site.js, so no JS means no element and the page keeps its flat ink ground.

   Drawn at ONE PIXEL PER CELL and scaled up here, which is why the rendering
   has to be pixelated: smoothing would blur the grid into the wash the shader's
   own comments warn against. The mask carves the 64% duty gaps that turn
   contiguous cells back into squares, the shader does that with a step()
   inside the cell, and this is the same figure by other means. */
.status-field {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 0;
  pointer-events: none;
  /* Drawn at one pixel per cell and scaled up here, so smoothing would blur the
     grid into exactly the wash the shader's own comments warn against. */
  image-rendering: pixelated;
  /* The mask that carves the 64% duty gaps, the shader does it with a step()
     inside each cell, this is the same figure by other means, is written by
     site.js from CELL, so the density lives in one place rather than two. */
}

/* Everything the page already had sits above it. The field is ground, not a
   layer things have to be lifted over, the rig and the beats already carry
   their own stacking, and this only has to be under them.

   .site-header is deliberately NOT in this list. It was, and because this rule
   comes hundreds of lines later it quietly overwrote the header's own
   `position: sticky` with `relative` at equal specificity, the header scrolled
   away and the sticky rule looked like it had simply not worked. It sets its
   own position and z-index where it is defined, which is the only place either
   should be decided. */
.page-shell,
.rig,
.site-footer { position: relative; z-index: 1; }

/* --- 14.6b1 The App Store badge ------------------------------------------ */

/* APPLE'S OWN ARTWORK, UNMODIFIED. That is the licence condition rather than a
   preference: the badge may not be redrawn, recoloured, stretched, rotated or
   rebuilt in CSS, so everything here styles the SPACE around it and nothing
   about the mark. The black variant carries its own white keyline and is the
   one Apple supplies for dark backgrounds.

   40px is their stated minimum height. The aspect is fixed by the file, so
   height alone sets it and width stays auto, anything else stretches the mark. */
.appstore {
  display: inline-flex;
  align-items: center;
  flex: none;
  line-height: 0;
  /* Clear space: Apple asks for at least 10% of the badge's height on every
     side, free of other elements. 4px is that at this size. */
  padding: 4px;
}
.appstore img {
  display: block;
  height: 40px;
  width: auto;
}

/* The hero's only action now that the button beside it is gone, so it starts
   the row rather than trailing something. */
.hero-actions .appstore { margin-left: calc(var(--space-sm) * -1); }

/* Centred under the closing line, where it is the last thing on the page and
   the only thing to do. */
/* THE CLOSING BLOCK, COMPACTED TWICE. It is a heading, one line and a badge
   inside a section padded for one that carries a whole FAQ, so it was sitting
   in a screen and a half of air. Space is the point at the top of a page and
   dead weight at the bottom.

   Measured before and after, because "half" is only meaningful against a
   number: the gap from the last question to the heading was 104px, the FAQ's
   own bottom padding plus this section's top, and the gap from the badge to
   the footer was 52px. Both are now about half that. The FAQ's bottom padding
   is halved HERE rather than on .section, so every other section on the site
   keeps the rhythm it was designed with. */
.section.center { padding-block: var(--space-lg); }
.section.center .lede { margin-top: var(--space-sm); }
#faq { padding-bottom: var(--space-xl); }

/* The badge centred as a block rather than as inline content. It is an
   inline-flex box with its own padding, and centring that by text-align leaves
   it a few pixels off its section's centre line, close enough to look like a
   mistake rather than an offset. margin-inline: auto centres the BOX. */
.section.center .appstore {
  display: flex;
  width: max-content;
  margin: var(--space-lg) auto 0;
}

/* The closing lede is centred COPY inside a centred section: .lede caps its own
   width for readability, and a capped block with no auto margin sits left of
   the heading above it rather than under it. */
.section.center .lede { margin-inline: auto; }

/* The licence credit, quieter than the trademark line above it. Attribution
   "in any manner reasonable to the medium" is what CC BY asks for; equal
   billing with the page's own copy is not. */
.footnote-fine {
  font-size: var(--text-2xs);
  opacity: 0.55;
  margin-top: var(--space-xs);
}

/* Until the listing exists these carry no href, so they are not links and must
   not pretend to be one. */
.appstore:not([href]) { cursor: default; }

@media (max-width: 46rem) {
  /* The nav wraps on narrow screens and this is the first thing that should
     go, the hero carries one already, a screen away. */
  .site-header .appstore { display: none; }
}

/* THE PLATFORM LINE. Quiet, and directly under the badge where the question it
   answers is being asked. A reader deciding whether to tap wants to know it is
   an iPhone app before they find out from the App Store, and saying it plainly
   reads as a choice rather than as a limitation being hidden. */
.platform {
  margin: var(--space-sm) 0 0;
  font-size: var(--text-sm);
  color: var(--text-faint);
}

/* --- 14.6b2 Selection ---------------------------------------------------- */

/* THE LANDING PAGE IS NOT SELECTABLE. Dragging across a page whose pointer is
   already a custom dot, and whose phone can be taken hold of, was painting blue
   over the composition on the way, the drag gesture belongs to the device now,
   and a selection highlight is the page arguing with it.

   THE LEGAL PAGES ARE. Privacy and Terms exist to be read closely, quoted and
   checked, and a support address exists to be copied, taking that away to stop
   a highlight on a marketing page would be a genuinely hostile trade, and on
   the legal pages arguably worse than hostile. `.prose` is the legal and
   support body copy and appears on no other page, so it is the exemption and
   it needs no markup change to carry.

   Form fields keep it unconditionally: text you can type is text you must be
   able to select, and there is no version of that which is a design choice. */
.page-shell {
  -webkit-user-select: none;
  user-select: none;
}

.prose,
.prose *,
input,
textarea,
select {
  -webkit-user-select: text;
  user-select: text;
}

/* --- 14.6b3 The wake ------------------------------------------------------ */

/* BEHIND THE DEVICE, ABOVE THE GROUND. z-index 0 puts it under everything the
   page lifted to 1, the rig included, so the phone occludes its own trail
   without anything having to know the trail exists. That is also what lets
   particles be born inside the device's box and only appear as it travels off
   them: they are hidden by the object they came from.

   NOT pixelated, unlike the field. The field is drawn one pixel per cell and
   scaled up, so smoothing would blur its grid into a wash; these are drawn at
   real size and move sub-pixel, where snapping would make them stutter. */
.wake {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  /* The script never starts, so this is only the floor for a page that somehow
     gets the element without the loop. */
  .wake { display: none; }
}

/* --- 14.6c The dot cursor ----------------------------------------------- */

/* The class is added by site.js only after it has checked for a fine pointer
   and for reduced motion, so nothing here can hide an arrow the reader still
   needs. No JS, touch device, or reduced motion: the class never lands and the
   system cursor is untouched. */
.has-dot-cursor,
.has-dot-cursor a,
.has-dot-cursor button,
.has-dot-cursor [role="button"],
.has-dot-cursor summary,
.has-dot-cursor label {
  cursor: none;
}
/* Text entry keeps its I-beam. The dot says where the pointer is; it cannot say
   "you may type here", and that is a different piece of information. */
.has-dot-cursor input,
.has-dot-cursor textarea,
.has-dot-cursor select {
  cursor: auto;
}

.dot-cursor {
  position: fixed;
  top: 0;
  left: 0;
  width: 10px;
  height: 10px;
  margin: -5px 0 0 -5px;      /* centred on the point, not hanging from it */
  border-radius: 999px;
  /* GOLD is the dot. White was left on it after the difference blend came off,
     which made the cursor read as white with a gold ring nobody sees until they
     hover, the opposite of "always gold". The white now belongs to the ring,
     where it is the state change rather than the identity. */
  background: var(--gold);
  /* GOLD AND WHITE, ALWAYS. This was mix-blend-mode: difference, which is a
     clever way to stay legible on any ground and a bad way to have a colour:
     difference INVERTS what is behind it, so the dot went cyan over the amber
     button, dark over pale video frames, and was never actually the two colours
     the rest of the page is made of. The cursor is part of the brand, not a
     readout of the pixels under it.
     The shadow is what difference was really buying, separation from whatever
     is behind, bought here without spending a colour on it. */
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.55));
  pointer-events: none;
  z-index: 9999;
  opacity: 0;
  /* Position comes from site.js every frame and is deliberately NOT
     transitioned, the ease lives in the driver, where it is a function of
     where the pointer is rather than of a duration. Size and opacity DO
     transition, because those are state changes rather than tracking. The DOT
     ITSELF never scales, see the ring below. */
  transition: opacity 220ms var(--ease);
  will-change: translate;
}
.dot-cursor[data-live] { opacity: 1; }

/* The affordance the hidden arrow used to carry, carried by a second ring
   rather than by growth. A dot that inflates to 3.4x stops being a cursor and
   becomes an object sitting on top of the thing it is pointing at, over the
   FAQ rows it covered the row. So the dot holds its size and a hairline ring
   appears a couple of pixels outside it: the same "you may click this", read
   at a glance, without anything being obscured. */
.dot-cursor::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 18px;                 /* 10px dot + a 2.5px gap on each side */
  height: 18px;
  margin: -9px 0 0 -9px;
  border-radius: 999px;
  border: 0.75px solid var(--stone-100);   /* hairline: it marks, it does not draw */
  opacity: 0;
  /* Only the RING scales, and only on the way in, it blooms out of the dot
     rather than fading on top of it, which is what makes the two read as one
     object changing state instead of two objects overlapping. */
  scale: 0.5;
  transition: opacity 200ms var(--ease), scale 260ms var(--ease);
}
/* The dot is gold in both states and the ring is the white. Making both gold
   turned the hot cursor into one gold blob, the two colours only read as two
   while each one has a job, and here gold is the identity and white is the
   state. */
.dot-cursor[data-hot]::after { opacity: 1; scale: 1; }
/* The press contracts the ring toward the dot. Same gesture the growth used to
   make, one order of magnitude quieter. */
.dot-cursor[data-down]::after { scale: 0.72; }

/* Belt and braces for the floor: if the media query ever changes under a live
   page, the dot goes and the arrow comes back without waiting for script. */
@media (prefers-reduced-motion: reduce), (hover: none), (pointer: coarse) {
  .dot-cursor { display: none; }
  .has-dot-cursor, .has-dot-cursor * { cursor: auto; }
}

/* --- 14.6e2 The legal modal ---------------------------------------------- */

/* A CARD OVER A BLURRED PAGE, not a section of it. Everything that went wrong
   with the expanding block came from the page changing length under the reader.
   A dialog cannot do that: the page behind it never moves at all.

   <dialog> carries the focus trap, Escape, the inert background and ::backdrop
   itself, so none of that is hand written here. */
.legal-modal {
  padding: 0;
  border: 0;
  background: none;
  max-width: min(72rem, 92vw);
  max-height: 86vh;
  width: 100%;
  overflow: hidden;
  color: var(--text-secondary);
}
.legal-modal::backdrop {
  /* 8px, not 22. At 22 the page behind stopped being a page and became a
     texture, which reads as the site having gone away rather than as something
     sitting in front of it. Blur cost also scales with radius, and this one
     covers the whole viewport. */
  background: rgba(6, 6, 8, 0.62);
  backdrop-filter: blur(8px) saturate(115%);
  -webkit-backdrop-filter: blur(8px) saturate(115%);
}

/* The card. A hard cap on its height with the body scrolling inside, so a long
   policy never grows the dialog off the screen. */
.legal-card {
  display: flex;
  flex-direction: column;
  max-height: 86vh;
  /* THE PAGE'S OWN SURFACE, the same one .card uses everywhere else. It was on
     --surface-strong, which is a step lighter and made the panel read as
     belonging to some other product: a grey box on a near black site. The site
     is built on --ink with --surface panels, and this is a panel. */
  background: var(--surface);
  color: var(--text-secondary);
  border: 1px solid var(--seam);
  border-radius: var(--radius-lg);
  box-shadow: 0 40px 120px rgba(0, 0, 0, 0.55);
  overflow: hidden;
}

/* Held out of the scroll, so the picker and the way out are reachable at any
   point in a document that is thousands of words long. This is what the sticky
   positioning was trying to do when it was part of the page, and a flex header
   over a scrolling body does it properly. */
.legal-head {
  flex: none;
  padding: var(--space-xl) var(--space-xl) var(--space-md);
  border-bottom: 1px solid var(--seam);
  text-align: center;
}
.legal-head .page-title { margin: 0; }
.legal-head .page-meta { margin: var(--space-2xs) 0 0; }

.legal-body {
  flex: 1 1 auto;
  min-height: 0;              /* or the flex child refuses to scroll */
  overflow-y: auto;
  overscroll-behavior: contain;  /* the page behind does not scroll with it */
  padding: var(--space-xl);
}

/* The documents get the card's full width. They are scanned rather than read
   through, and the reference tables need the room. */
.legal-modal .prose { max-width: none; }
.legal-panel[hidden] { display: none; }

@media (max-width: 46rem) {
  .legal-modal { max-width: 96vw; max-height: 92vh; }
  .legal-card { max-height: 92vh; }
  .legal-head, .legal-body { padding-inline: var(--space-md); }
}

@media (prefers-reduced-motion: no-preference) {
  .legal-modal[open] { animation: legal-in 260ms var(--ease); }
  .legal-modal[open]::backdrop { animation: legal-fade 260ms var(--ease); }
}
@keyframes legal-in {
  from { opacity: 0; transform: translateY(12px) scale(0.985); }
  to   { opacity: 1; transform: none; }
}
@keyframes legal-fade { from { opacity: 0; } to { opacity: 1; } }

/* The picker and the way out, on one row and next to each other. The pair
   centres as a GROUP: centring the picker and throwing the button to the far
   edge left it belonging to nothing, a stray control on the other side of the
   card from the thing it acts on.

   No longer sticky. It lives in the card's fixed header, held out of the scroll
   by flex, so it stays put without being told to. */
.legal-controls {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: var(--space-sm);
  margin: var(--space-md) 0 0;
}

/* No margin of its own. Flex centres MARGIN boxes, so a bottom margin here
   lifts the picker's border box above the button beside it: aligned by the
   rule, visibly off by the eye. Spacing belongs to the row. */
.legal-modal .segmented {
  display: grid;
  width: max-content;
  margin: 0;
}

/* An X beside the picker rather than a fourth pill inside it. The picker
   chooses WHICH document; putting "close" among "privacy, terms, support" puts
   an action in a list of nouns, and every tap on it teaches the wrong thing
   about the other three. */
.legal-close {
  appearance: none;
  cursor: pointer;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  font: inherit;
  font-size: 1.35rem;
  line-height: 1;
  color: var(--text-secondary);
  background: transparent;
  border: 1px solid var(--seam);
  border-radius: 999px;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.legal-close:hover { color: var(--text); border-color: var(--gold); }
.legal-close:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }

/* THE PAGE BEHIND DOES NOT SCROLL while the card is open. A dialog makes the
   background inert to clicks and to the keyboard, but not to the wheel, and a
   page sliding about behind a blur is the thing that makes a modal feel like it
   is not really in front. */
.legal-locked { overflow: hidden; }

/* --- 14.6f The segmented control ----------------------------------------- */

/* The iOS picker, the one the selfie flow uses to choose male or female: a
   small set of peers in a track, with a thumb that slides to the chosen one.
   The right control when the options are few, equal, and mutually exclusive,
   which is exactly three legal documents.

   The thumb is a separate element sliding under the labels rather than a
   background on the selected button, because a background cannot animate
   between two elements. Positioned from a custom property the script sets, so
   the movement is one transform rather than a re-layout. */
.segmented {
  position: relative;
  display: inline-grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 2px;
  padding: 3px;
  margin: var(--space-xl) 0 var(--space-2xl);
  border-radius: 999px;
  background: var(--surface-strong);
  border: 1px solid var(--seam);
  isolation: isolate;
}

.seg {
  position: relative;
  z-index: 1;
  appearance: none;
  border: 0;
  background: none;
  cursor: pointer;
  padding: 0.5rem 1.35rem;
  border-radius: 999px;
  font: inherit;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  white-space: nowrap;
  transition: color var(--dur) var(--ease);
}
.seg:hover { color: var(--text); }
.seg[aria-selected="true"] { color: var(--ink); }
.seg:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }

.seg-thumb {
  position: absolute;
  z-index: 0;
  top: 3px;
  bottom: 3px;
  left: 3px;
  width: var(--seg-w, 0px);
  translate: var(--seg-x, 0px) 0;
  border-radius: 999px;
  background: var(--gold);
  transition: translate 280ms var(--ease), width 280ms var(--ease);
}

/* No JS: no thumb, and every panel open. A legal page that hides two thirds of
   itself when a script fails is the one kind of page that must not.

   Opening the panels was never enough on its own. A <dialog> with no `open`
   attribute is display:none in the UA sheet, so the panels below computed
   block at zero height inside a hidden ancestor and the page served nothing.
   Apple requires the Privacy Policy URL to lead to the policy, and a crawler
   or a reviewer with scripting off was getting a blank marketing page. So the
   dialog itself has to come back into the normal flow, and every cap that
   exists to keep a floating card on screen has to come off, because without
   JS this is not a card, it is the document. */
.no-js .seg-thumb { display: none; }
.no-js .seg[aria-selected="true"] { color: var(--text); text-decoration: underline; }
.no-js .legal-panel[hidden] { display: block; }
.no-js .legal-modal {
  display: block;
  position: static;
  max-width: min(72rem, 92vw);
  max-height: none;
  overflow: visible;
  margin: var(--space-2xl) auto;
}
.no-js .legal-card { max-height: none; overflow: visible; }
.no-js .legal-body { overflow-y: visible; }
/* Nothing to close when there was never anything to open. */
.no-js .legal-close { display: none; }

@media (prefers-reduced-motion: reduce) {
  .seg-thumb { transition: none; }
}

.legal-panel[hidden] { display: none; }

/* Mid-close. Still in the layout, holding the page's height, but invisible, so
   the travel happens against a page that is not changing size and the removal
   at the end changes nothing anybody can see. */
.legal[data-closing] { opacity: 0; pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
  .legal { transition: none; }
}

/* --- 14.7 Two classes the dynamic page needs -------------------------- */

/* The skip link. Off-screen until focused, then it lands in the corner. */
.skip-link {
  position: absolute;
  left: var(--gutter);
  top: 0;
  transform: translateY(-140%);
  z-index: 20;
  padding: 0.6rem 1rem;
  border-radius: var(--radius-sm);
  background: var(--surface-strong);
  color: var(--text);
  border: 1px solid var(--seam);
  text-decoration: none;
  transition: transform var(--dur) var(--ease);
}
.skip-link:focus-visible { transform: translateY(0.6rem); }

/* The hero headline. Same display treatment as .hero h1, over a picture. */
.reel-title {
  font-size: var(--text-display);
  line-height: var(--leading-display);
  letter-spacing: var(--track-display);
  max-width: 15ch;
  text-shadow: 0 2px 40px rgba(0, 0, 0, 0.55);
}
/* One word in the accent. The sentence is the product's whole claim and
   "cinema" is the half of it that is the promise; colouring the noun rather
   than the verb keeps it a statement instead of a slogan. */
.reel-title .hi { color: var(--gold); }

.reel-copy .lede {
  font-size: var(--text-xl);
  max-width: 46ch;
  color: var(--text);
}

