/* ===========================================================================
   Doromir — Lucid Psychedelia, web edition.

   Every token below is lifted from dream-app/mobile/theme/lucid.js so the site
   and the app cannot drift. The system is neo-brutalism (heavy 2px borders, hard
   non-blurred offset shadows, pill actions) over organic surrealism (the ambient
   marble shader running behind everything).

   Two radii, no more — `--r-card` for every frame, `--r-pill` for everything you
   tap. Anything else is drift; the app learned this the hard way when App.js had
   10/14/16/17/18/20/22/24/28 all at once.
   =========================================================================== */

/* ── Fonts ────────────────────────────────────────────────────────────────────
   Self-hosted, all three families redistributable: Guanine is CC0, Anybody and
   Work Sans are SIL Open Font License 1.1. Static instanced weights — pick the
   weight by family, never lean on synthetic bolding. `font-display: swap` so
   dense legal prose is readable before the webfont lands. */
@font-face {
  font-family: 'Guanine';
  src: url('../fonts/Guanine.otf') format('opentype');
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: 'Anybody';
  src: url('../fonts/Anybody-Regular.ttf') format('truetype');
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: 'Anybody';
  src: url('../fonts/Anybody-Bold.ttf') format('truetype');
  font-weight: 700;
  font-display: swap;
}
@font-face {
  font-family: 'Anybody';
  src: url('../fonts/Anybody-ExtraBold.ttf') format('truetype');
  font-weight: 800;
  font-display: swap;
}
@font-face {
  font-family: 'Work Sans';
  src: url('../fonts/WorkSans-Regular.ttf') format('truetype');
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: 'Work Sans';
  src: url('../fonts/WorkSans-Medium.ttf') format('truetype');
  font-weight: 500;
  font-display: swap;
}
@font-face {
  font-family: 'Work Sans';
  src: url('../fonts/WorkSans-SemiBold.ttf') format('truetype');
  font-weight: 600;
  font-display: swap;
}
@font-face {
  font-family: 'Work Sans';
  src: url('../fonts/WorkSans-Bold.ttf') format('truetype');
  font-weight: 700;
  font-display: swap;
}

/* ── Tokens ─────────────────────────────────────────────────────────────────*/
:root {
  /* Canvas is pure black; the shader sits behind the UI layer. */
  --bg: #000000;

  /* Rose / salmon — the primary "on-dark" ink for display type, borders, chips. */
  --rose: #ffdad4;
  /* Tertiary teal — the brand wordmark and the primary action. */
  --teal: #b0ecfa;
  --teal-ink: #001f25;
  /* Marigold — accent, used sparingly. */
  --marigold: #ffdf9f;
  --error: #ba1a1a;

  --white: #ffffff;
  --body-dim: #d1d5db;
  --muted: #b8c0cc;
  --muted-dim: #6b7280;

  /* Surfaces — translucent glass over the dark canvas. */
  --card-fill: rgba(30, 30, 30, 0.7);
  --glass-fill: rgba(0, 0, 0, 0.6);
  --glass-soft: rgba(0, 0, 0, 0.5);
  --chip-fill: rgba(255, 255, 255, 0.1);

  /* Borders. */
  --border-rose: #ffdad4;
  --border-rose-50: rgba(255, 218, 212, 0.5);
  --border-rose-faint: rgba(226, 190, 184, 0.3);
  --border-w10: rgba(255, 255, 255, 0.1);
  --border-w20: rgba(255, 255, 255, 0.2);

  /* Hard-shadow tints — the element's own colour at 50%. */
  --shadow-rose: rgba(255, 218, 212, 0.5);
  --shadow-teal: rgba(176, 236, 250, 0.5);

  --r-card: 12px;
  --r-pill: 9999px;
  /* A third radius, and the only one the two-radii rule allows. A device bezel is
     a picture of a physical object, not a UI frame — at --r-card a phone reads as
     a tablet. Named so it stays an exception rather than becoming drift. */
  --r-device: 38px;
  --bw-hair: 2px;
  --bw-accent: 4px;

  /* How far a revealed element rises into place. One value so every reveal on the
     page travels the same distance. */
  --reveal-lift: 24px;

  /* 8px rhythm; 20px margins on mobile, 64px on desktop, 1280px container. */
  --gutter: 24px;
  --margin: 20px;
  --container: 1280px;
  --prose: 74ch;
}

@media (min-width: 900px) {
  :root {
    --margin: 64px;
  }
}

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

html {
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

body {
  margin: 0;
  min-height: 100vh;
  background: var(--bg);
  color: var(--body-dim);
  font-family: 'Work Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
  font-size: 18px;
  line-height: 28px;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* The shader canvas is fixed behind everything. Before it initialises (and
   forever, if WebGL is unavailable) this gradient stands in — same pastel ring,
   frozen. Without it a WebGL-less browser gets a flat black page. */
#stream {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: -2;
  pointer-events: none;
  /* Promote to its own compositing layer. Both this and the scrim are fixed over a
     scrolling document; without the promotion Chrome repaints them on every scroll
     tick and a fast flick can outrun the paint, flashing black behind the content. */
  will-change: transform;
  /* Stand-in for browsers without WebGL: the shader's "Ink" ring flattened to four
     static blooms. These are the same six stops from stream.js converted to 8-bit —
     change one and change the other, or a WebGL-less browser gets a palette the
     shader no longer uses. */
  background:
    radial-gradient(120% 80% at 12% 0%, rgba(173, 184, 204, 0.24), transparent 60%),
    radial-gradient(110% 70% at 88% 22%, rgba(143, 153, 179, 0.22), transparent 62%),
    radial-gradient(120% 90% at 30% 78%, rgba(122, 133, 158, 0.22), transparent 64%),
    radial-gradient(100% 70% at 82% 96%, rgba(92, 102, 128, 0.2), transparent 60%);
}

/* Once the shader is drawing, drop the CSS stand-in so the two never composite. */
#stream.is-live {
  background: none;
}

/* Scrim between the shader and the content — the web counterpart of the app's
   EdgeScrim (mobile/theme/EdgeScrim.js). The app scrims only the top and bottom,
   because on a phone the middle of the screen is covered by opaque glass cards
   anyway. A desktop page has text and open space everywhere, so the flat portion
   runs the full height and the edges darken further, seating the header lockup and
   the footer on a dark base exactly as the app does.

   The app's canvas is pure black with the shader as atmosphere over it. Without
   this the proportions invert at desktop width: the marble becomes the page and
   the black canvas disappears. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  will-change: transform;
  background: linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0.86) 0,
    rgba(0, 0, 0, 0.52) 200px,
    rgba(0, 0, 0, 0.52) calc(100% - 200px),
    rgba(0, 0, 0, 0.86) 100%
  );
}

img {
  max-width: 100%;
  height: auto;
}

a {
  color: var(--teal);
  text-decoration-color: rgba(176, 236, 250, 0.4);
  text-underline-offset: 3px;
  transition: color 0.15s ease, text-decoration-color 0.15s ease;
}

a:hover {
  color: var(--white);
  text-decoration-color: currentColor;
}

:focus-visible {
  outline: 3px solid var(--marigold);
  outline-offset: 3px;
  border-radius: 4px;
}

/* Skip link — visually hidden until focused. */
.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 12px 20px;
  background: var(--teal);
  color: var(--teal-ink);
  font-weight: 600;
  border-radius: 0 0 var(--r-card) 0;
}
.skip:focus {
  left: 0;
}

.wrap {
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 var(--margin);
}

/* ── Type ───────────────────────────────────────────────────────────────────
   Guanine carries every heading (the app's `font.heading`). Anybody is reserved
   for numerals and display figures, exactly as in the app, where the clock stays
   in Anybody for glance clarity. Work Sans is body and labels. */
h1,
h2,
h3 {
  font-family: 'Guanine', 'Anybody', system-ui, sans-serif;
  font-weight: 400;
  color: var(--rose);
  margin: 0;
  letter-spacing: 0;
}

h1 {
  font-size: clamp(2.5rem, 6.2vw, 4.5rem);
  line-height: 1.06;
  color: var(--white);
}

h2 {
  font-size: clamp(1.75rem, 3.2vw, 2.25rem);
  line-height: 1.24;
}

h3 {
  font-size: 1.25rem;
  line-height: 1.35;
  color: var(--white);
}

p {
  margin: 0 0 1rem;
}

p:last-child {
  margin-bottom: 0;
}

strong {
  color: var(--white);
  font-weight: 600;
}

/* Uppercase technical label — Work Sans semibold with generous tracking. */
.kicker {
  font-size: 14px;
  line-height: 20px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--marigold);
  margin: 0 0 12px;
}

.lede {
  font-size: clamp(1.125rem, 1.8vw, 1.375rem);
  line-height: 1.6;
  color: var(--body-dim);
  max-width: 56ch;
}

.small {
  font-size: 14px;
  line-height: 22px;
  color: var(--muted);
}

/* ── Page header — the shared Doromir lockup ────────────────────────────────
   Mirrors mobile/theme/PageHeader.js: white crescent mark + teal wordmark, a 4px
   rule beneath, page name under that. One definition; no page rolls its own. */
.site-header {
  padding-top: 28px;
}

.brand-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gutter);
  flex-wrap: wrap;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
}

/* An <img> rather than inline SVG: the file carries a C2PA provenance block that
   dwarfs its own artwork, so inlining it would repeat ~28KB on all six pages
   instead of fetching it once into the cache. The mark is multi-colour, so it has
   no currentColor to inherit and loses nothing by being external. */
.brand-mark {
  width: 40px;
  height: 40px;
  flex: none;
}

/* Teal wordmark, normal case. Teal ties the brand to the primary capture action
   and — unlike rose — is not also the body ink, so the lockup reads as distinct
   from page text. */
.brand-name {
  font-family: 'Guanine', system-ui, sans-serif;
  font-size: 26px;
  line-height: 32px;
  letter-spacing: 0.3px;
  color: var(--teal);
}

.brand:hover .brand-name {
  color: var(--white);
}

/* The header carries the brand lockup and four actions — the thing you came to do
   (Download) and the three the product answers with: what it costs, how it works,
   and what has changed. Privacy, Terms and data deletion stay in the footer, which
   is where people look for them and where Play's review expects them.

   Only one of the four is styled as a button. A row of equal-weight buttons has no
   primary, and the header's job is to make the download obvious. */
.header-actions {
  display: flex;
  align-items: center;
  gap: 22px;
}

.header-link {
  font-family: 'Work Sans', system-ui, sans-serif;
  font-size: 14px;
  line-height: 20px;
  color: var(--muted);
  text-decoration: none;
  white-space: nowrap;
}

.header-link:hover {
  color: var(--white);
  text-decoration: underline;
}

/* The FAQ page's own header link. Rose rather than muted, matching how the app
   marks the tab you are on. */
.header-link[aria-current='page'] {
  color: var(--rose);
}

@media (max-width: 500px) {
  .brand-mark {
    width: 32px;
    height: 32px;
  }

  .brand-name {
    font-size: 22px;
  }

  .header-actions {
    gap: 14px;
  }

  /* Four items where the wide layout has room for four and a half. The button
     keeps its height, so the touch target is unchanged; only the air either side
     of the word comes in. */
  .header-actions .btn--sm {
    padding: 10px 16px;
  }
}

/* Below about 440px the brand lockup and four actions stop sharing a line, and
   `.brand-row` wraps. Left alone the actions land under the wordmark hugging the
   left edge, which reads as an overflow rather than a decision. Giving them the
   whole second row and spreading them across it makes it a two-row header
   instead — and the tighter row gap keeps the two halves reading as one lockup.

   Measured, not guessed, and re-measured when the blog took a fourth slot:
   untightened the row needs 494px, and at the sizes set above it needs 440px.
   Both queries move together — a gap between them is the left-hugging overflow
   this one exists to prevent, and re-titling a link is enough to open one. */
@media (max-width: 440px) {
  .brand-row {
    row-gap: 14px;
  }

  .header-actions {
    width: 100%;
    justify-content: space-between;
  }
}

/* Rule directly under the brand row. Matches the app's section-header rule
   exactly, so the system has one divider treatment rather than two. */
.rule {
  height: var(--bw-accent);
  background: var(--border-w10);
  border: 0;
  margin: 18px 0 0;
}

/* ── Chips ──────────────────────────────────────────────────────────────────*/
.chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 9px 18px;
  border-radius: var(--r-pill);
  border: var(--bw-hair) solid var(--border-rose-50);
  background: var(--chip-fill);
  color: var(--rose);
  font-size: 12px;
  line-height: 16px;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.chip svg {
  width: 14px;
  height: 14px;
}

.chip--teal {
  border-color: rgba(176, 236, 250, 0.5);
  color: var(--teal);
}

/* A chip introducing a page sits on its own line, and the block under it needs
   air the inline box does not provide. Matches the 26px the FAQ page gets from
   `.page-title`'s own top margin. */
.hero .chip {
  margin-bottom: 26px;
}

/* On the pages that open with a chip rather than a title, it lands about five
   pixels under the header rule, because `.page-title` is what carried the
   clearance and there is no title above the chip to provide it. Matches the
   26px `.page-title` sets, so a page reads the same whichever it opens with. */
main > .wrap > .chip:first-child {
  margin-top: 26px;
}

/* ── Buttons ────────────────────────────────────────────────────────────────
   Large, uppercase, 2px border, hard 4px offset shadow. On hover the shadow
   expands to 6px and the element translates -2px on both axes, simulating a
   physical press. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 16px 30px;
  border-radius: var(--r-pill);
  border: var(--bw-hair) solid var(--teal);
  background: var(--teal);
  color: var(--teal-ink);
  font-family: 'Anybody', system-ui, sans-serif;
  font-size: 16px;
  font-weight: 700;
  line-height: 24px;
  letter-spacing: -0.4px;
  text-transform: uppercase;
  text-decoration: none;
  box-shadow: 4px 4px 0 var(--shadow-teal);
  transition: box-shadow 0.15s ease, transform 0.15s ease;
}

.btn:hover {
  color: var(--teal-ink);
  transform: translate(-2px, -2px);
  box-shadow: 6px 6px 0 var(--shadow-teal);
}

.btn:active {
  transform: translate(0, 0);
  box-shadow: 2px 2px 0 var(--shadow-teal);
}

.btn svg {
  width: 20px;
  height: 20px;
}

.btn .play-icon {
  width: 20px;
  height: 20px;
  flex: none;
}

/* Header-sized. The press physics are the same gesture at a smaller scale — the
   shadow and its hover growth shrink with the button, so it reads as the same
   object rather than a different one. */
.btn--sm {
  padding: 10px 20px;
  font-size: 13px;
  line-height: 20px;
  box-shadow: 3px 3px 0 var(--shadow-teal);
}

.btn--sm:hover {
  box-shadow: 5px 5px 0 var(--shadow-teal);
}

.btn--sm:active {
  box-shadow: 2px 2px 0 var(--shadow-teal);
}

.btn--sm svg {
  width: 16px;
  height: 16px;
}

.btn--ghost {
  background: var(--glass-fill);
  border-color: var(--border-rose);
  color: var(--rose);
  box-shadow: 4px 4px 0 var(--shadow-rose);
}

.btn--ghost:hover {
  color: var(--white);
  box-shadow: 6px 6px 0 var(--shadow-rose);
}

.btn--ghost:active {
  box-shadow: 2px 2px 0 var(--shadow-rose);
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  align-items: center;
  margin-top: 36px;
}

/* `.closer` centres its text, but a flex row does not inherit that. Without this the
   row still starts at the left edge of the section and the button sits under nothing.
   A modifier rather than a `.closer .btn-row` descendant rule, so the centred row
   stays available to any section that wants it. */
.btn-row--center {
  justify-content: center;
}

/* ── Surfaces ───────────────────────────────────────────────────────────────*/
.glass {
  background: var(--glass-fill);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  border: var(--bw-hair) solid var(--border-w10);
  border-radius: var(--r-card);
}

/* ── Device frame ───────────────────────────────────────────────────────────
   The page's one piece of illustration. Drawn in CSS rather than shipped as a
   mockup PNG so it inherits the system's border weight and hard offset shadow,
   and so the screenshot inside stays a real image at real resolution.

   The screen is a fixed 780:1688 window — the source captures' viewport ratio.
   Two of the four screenshots are full-scroll captures much taller than that;
   they overflow deliberately and reveal.js pans them (see [data-scrollshot]). */
.phone {
  position: relative;
  width: 100%;
  max-width: 320px;
  margin: 0 auto;
  padding: 10px;
  background: #000;
  border: var(--bw-hair) solid var(--border-rose-faint);
  border-radius: var(--r-device);
  box-shadow: 8px 8px 0 var(--shadow-rose);
  transition: box-shadow 0.2s ease, border-color 0.2s ease;
}

.phone:hover {
  border-color: var(--border-rose-50);
  box-shadow: 10px 10px 0 var(--shadow-rose);
}

.phone-screen {
  position: relative;
  overflow: hidden;
  aspect-ratio: 780 / 1688;
  border-radius: calc(var(--r-device) - 12px);
  background: #000;
}

.phone-screen img {
  display: block;
  width: 100%;
  height: auto;
  /* The viewport-height captures match the frame's ratio to within a rounding
     error, which can leave a sub-pixel strip of bare screen at the bottom edge.
     min-height closes it and object-fit absorbs the difference by cropping rather
     than stretching. Neither touches the full-scroll captures, whose auto height
     is several times the frame. */
  min-height: 100%;
  object-fit: cover;
  object-position: top center;
  /* Panned by reveal.js on tall captures; the transform origin must be the top so
     progress 0 shows the top of the screen. */
  transform-origin: top center;
  will-change: transform;
}

/* An empty screen running the app's ambient background instead of a capture —
   the last showcase row. Sized by its own box rather than the viewport (see the
   `element` branch in stream.js), so it fills the frame at every width.

   The gradient is the stand-in for browsers without WebGL, the same job the one
   on `#stream` does: the Teal ring's six stops flattened to four blooms. Change
   one and change the other. */
.phone-stream {
  display: block;
  width: 100%;
  height: 100%;
  background:
    radial-gradient(120% 80% at 18% 4%, rgba(62, 208, 224, 0.3), transparent 60%),
    radial-gradient(110% 70% at 84% 26%, rgba(30, 169, 187, 0.28), transparent 62%),
    radial-gradient(120% 90% at 26% 74%, rgba(21, 121, 132, 0.28), transparent 64%),
    radial-gradient(100% 70% at 80% 98%, rgba(124, 223, 234, 0.24), transparent 60%);
}

/* Once the shader is drawing, drop the stand-in so the two never composite. */
.phone-stream.is-live {
  background: none;
}

/* The brand mark centred on that empty screen. Needs the extra `.phone-screen`
   qualifier to outscore the screenshot rules above, which would otherwise stretch
   it to the full width of the frame and crop it to the top edge.

   Sized as a share of the frame rather than in pixels, so it holds its proportion
   as the device scales — the app draws the same mark at 68-104pt on a phone, which
   is about this fraction of the screen. The artwork's interior is transparent, so
   the stream shows through the letterforms; nothing here paints a backing. */
.phone-screen .phone-mark {
  position: absolute;
  left: 50%;
  top: 50%;
  translate: -50% -50%;
  width: 44%;
  height: auto;
  min-height: 0;
  object-fit: contain;
  transform-origin: center;
  will-change: auto;
}

.phone--tilt {
  transform: rotate(-3deg);
}

/* The app's capture orb, bloomed out behind the hero device. Teal rather than the
   screenshot's green: on the page this is brand atmosphere, and teal is what the
   system already spends on the capture action. */
.phone-glow {
  position: absolute;
  left: 50%;
  top: 46%;
  width: min(460px, 92%);
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle, rgba(176, 236, 250, 0.42), transparent 66%);
  filter: blur(44px);
  pointer-events: none;
  animation: bloom 9s ease-in-out infinite;
}

/* ── Layout ─────────────────────────────────────────────────────────────────*/
section {
  padding: clamp(56px, 8vw, 104px) 0;
}

.hero {
  padding-top: clamp(40px, 6vw, 76px);
  padding-bottom: clamp(40px, 6vw, 72px);
}

/* The headline is a two-line lockup: "Speak your dream" / "before it fades."
   Both halves have to fit, which pins the h1's width to a band measured against
   its own font: "Speak your dream" is 8.48em wide, and "Speak your dream before
   it" is 12.70em. Anything narrower than 8.48em drops to three lines; anything
   from 12.70em up pulls "before it" onto line one and orphans "fades.".

   15ch lands at 10.88em, near the middle of that band, and the hero's own
   font-size clamp below keeps the column-to-type ratio inside it at every width.
   Retype the headline and both numbers move, so re-measure rather than nudging. */
.hero-grid h1 {
  max-width: 15ch;
  /* Gentler than the global h1's 6.2vw. Between 900px and 1333px the shared scale
     outgrows the hero's copy column and forces a third line; this keeps the two
     in step. Identical to the global scale at both ends, where the 2.5rem floor
     and the 4.5rem ceiling take over. */
  font-size: clamp(2.5rem, 5.4vw, 4.5rem);
}

.hero .lede {
  margin-top: 24px;
  max-width: 40ch;
}

/* Copy first in the DOM at every width, so the h1 is never pushed below an image
   on a phone. Below 900px the columns simply stack in that order. */
.hero-grid {
  display: grid;
  gap: clamp(40px, 6vw, 72px);
  align-items: center;
}

.hero-art {
  position: relative;
  display: flex;
  justify-content: center;
}

.hero-art .phone {
  max-width: 300px;
}

/* The app's dream orb at hero scale. Square, because the shader corrects for
   aspect and a non-square canvas just wastes pixels around a circle.

   It has no box of its own — no border, no frame, no shadow. The orb ends in a
   halo that fades to nothing, and any edge drawn around that would be an edge the
   app does not have. Everything else on this page is neo-brutalist and hard-edged;
   this is the one soft thing, which is precisely why it reads. */
.orb {
  display: block;
  width: clamp(220px, 32vw, 400px);
  aspect-ratio: 1;
  /* Stand-in until the shader compiles, and permanently where WebGL is missing:
     the same emerald core and mint halo, frozen mid-breath. */
  background: radial-gradient(
    circle,
    rgba(71, 204, 143, 0.95) 0,
    rgba(71, 204, 143, 0.9) 24%,
    rgba(140, 235, 189, 0.34) 42%,
    transparent 62%
  );
  border-radius: 50%;
}

.orb.is-live {
  background: none;
  border-radius: 0;
}

@media (min-width: 900px) {
  /* The copy column carries a two-line headline at this size, so it takes the
     larger share. The art column holds only the orb, which is a soft radial with
     no detail to lose, so it gives up the width. */
  .hero-grid {
    grid-template-columns: 1.25fr 0.75fr;
  }

  .hero-art .phone {
    max-width: 340px;
  }

  /* Below this width the columns stack and the orb is centred under the copy;
     once there is a second column it belongs to that column's centre, which is
     already what `.hero-art` does. Nothing to add — the size clamp carries it. */
}

/* Duolingo's stacked full-width action pair on a phone; inline once there is
   room. The button's own press physics is unchanged. */
.btn-row--stack {
  flex-direction: column;
  align-items: stretch;
}

@media (min-width: 560px) {
  .btn-row--stack {
    flex-direction: row;
    align-items: center;
  }
}

/* ── Showcase ───────────────────────────────────────────────────────────────
   One screen, one headline, one line. Art first in the DOM so that when the
   columns stack you see the screen and then read what it is. */
.showcase-set {
  padding-top: clamp(48px, 7vw, 88px);
  padding-bottom: clamp(48px, 7vw, 88px);
}

.showcase {
  display: grid;
  gap: clamp(28px, 5vw, 80px);
  align-items: center;
}

.showcase-copy {
  max-width: 46ch;
}

.showcase-copy h2 {
  margin-top: 4px;
}

/* :not(.kicker) is load-bearing, not defensive. A bare `.showcase-copy p` scores
   0,1,1 against .kicker's 0,1,0, so it silently repaints every section label from
   marigold to muted grey — the label stops reading as a label and the page loses
   its only accent colour. Same guard as .closer below. */
.showcase-copy p:not(.kicker) {
  color: var(--muted);
  margin-top: 14px;
}

@media (min-width: 860px) {
  .showcase {
    grid-template-columns: 0.9fr 1.1fr;
  }

  /* Flip only the painted order, never the DOM order — the art stays first for a
     screen reader and for the stacked layout.

     Deliberately no `justify-self: end` on the flipped copy. It looks like the
     tidier choice and it does work, but the copy box is 46ch inside a wider
     column, so all it buys is an ~80px inset — and that inset pushes the text off
     the page margin that the header, the pledge and the footer all sit on. A
     straight left edge down the whole page is worth more than closing a gap. */
  .showcase--flip .showcase-art {
    order: 2;
  }

  .showcase--flip .showcase-copy {
    order: 1;
  }

  .showcase--flip {
    grid-template-columns: 1.1fr 0.9fr;
  }
}

/* Bigger than the hero device: these carry the journal and insights captures,
   whose real UI text has to stay readable at rest. */
.showcase-set .phone {
  max-width: 360px;
}

/* Copy between two devices. Two columns below the breakpoint so the phones sit
   side by side with the copy spanning underneath; three above it, with the copy
   placed into the middle column of the same row.

   Must come after the 860px block above, not inside it — `.showcase--split` and
   `.showcase` score the same specificity, so source order is the whole argument. */
.showcase--split {
  grid-template-columns: 1fr 1fr;
}

.showcase--split .showcase-copy {
  grid-column: 1 / -1;
}

@media (min-width: 860px) {
  .showcase--split {
    grid-template-columns: 1fr 1.25fr 1fr;
    align-items: center;
  }

  .showcase-art--left {
    grid-column: 1;
    grid-row: 1;
  }

  .showcase-art--right {
    grid-column: 3;
    grid-row: 1;
  }

  /* The copy box is centred inside its interior column, but the text itself is
     ragged right like every other block on the page — one reading edge whether a
     row is flipped, split, or plain. */
  .showcase--split .showcase-copy {
    grid-column: 2;
    grid-row: 1;
    margin: 0 auto;
    text-align: left;
  }
}

/* ── Roadmap card ───────────────────────────────────────────────────────────
   Sits inside a showcase column, so it takes the glass surface rather than the
   pledge's full active-surface treatment (coloured border plus hard shadow).
   That treatment is what the page spends on its one loud claim; a list of things
   that do not exist yet has not earned it, and two panels shouting at that
   volume would leave the pledge with nothing to be louder than. */
.roadmap {
  margin-top: 28px;
  padding: 20px 22px;
}

/* Three classes, and that is the point: `.showcase-copy p:not(.kicker)` scores
   0,2,1 and would otherwise repaint this line to --muted, flattening the card's
   one piece of hierarchy. Same trap the showcase kicker documents above — a
   `p` selector in this column needs to outscore that rule or it loses. */
.showcase-copy .roadmap .roadmap-title {
  margin: 0;
  color: var(--white);
  font-size: 15px;
  line-height: 24px;
  font-weight: 600;
}

.roadmap ul {
  margin: 14px 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 10px;
}

.roadmap li {
  display: grid;
  grid-template-columns: 14px 1fr;
  align-items: start;
  gap: 12px;
  color: var(--muted);
  font-size: 15px;
  line-height: 24px;
}

/* A hollow ring, not the pledge's drawn checkmark. The shape carries the tense:
   the pledge ticks things that are true today, this one marks things that are
   not built. Marigold because the system already spends it on accents, and
   because it is not the teal the pledge uses for what it can promise. */
.roadmap li::before {
  content: '';
  width: 9px;
  height: 9px;
  margin-top: 7px;
  border: 2px solid var(--marigold);
  border-radius: 50%;
}

/* Fine print under a showcase claim — the conditions attached to it. Small, and
   --muted-dim would fail contrast at this size, so it stays on --muted and earns
   its quietness from size and width instead. */
.fineprint {
  font-size: 13px;
  line-height: 20px;
  max-width: 42ch;
  color: var(--muted);
}

.showcase-copy .fineprint,
.hero-copy .fineprint {
  margin-top: 16px;
}

/* ── Dream-map focus cycle ──────────────────────────────────────────────────
   Real captures of one map with a different tag focused in each, stacked in the
   phone screen. The bottom layer is the unfiltered map and never animates: it is
   the resting pose, so the reduced-motion collapse (which reverts each overlay to
   its `opacity: 0` base) lands on a complete picture rather than an empty frame.
   The same rule as the marquee and the float — see the Motion section. */
.map-cycle > .map-focus {
  position: absolute;
  inset: 0;
  opacity: 0;
  animation: mapFocus 17s ease-in-out infinite;
}

.map-cycle > .map-focus img {
  height: 100%;
}

/* A quarter of the cycle each, in DOM order. */
.map-cycle > .map-focus:nth-of-type(2) {
  animation-delay: 0s;
}
.map-cycle > .map-focus:nth-of-type(3) {
  animation-delay: 4.25s;
}
.map-cycle > .map-focus:nth-of-type(4) {
  animation-delay: 8.5s;
}
.map-cycle > .map-focus:nth-of-type(5) {
  animation-delay: 12.75s;
}

/* ── Showcase bands ───────────────────────────────────────────
   Two of the five showcase rows sit on a band, so the set reads as alternating
   ground rather than one uninterrupted scroll of black.

   The fill is a film of white rather than a wash of black, because the thing
   behind it is the shader. Darkening would hide the ambient motion exactly where
   the band is; lightening lets it through and tints it, so the band reads as a
   pane laid over the page rather than a hole cut in it. It is deliberately near
   the limit of what is visible — the effect wanted here is "something changed",
   not "here is a panel".

   The 2px edges are the same ink `.rule` used, and they do the job the rules
   between these rows used to do. */
.band {
  background: rgba(255, 255, 255, 0.03);
  border-top: var(--bw-hair) solid var(--border-w10);
  border-bottom: var(--bw-hair) solid var(--border-w10);
  padding: var(--row-gap) 0;
}

/* One gap, spent either as a band's padding or as an unbanded row's margin, so
   the rhythm down the set is unchanged from when hairline rules carried it. */
.showcase-set {
  --row-gap: clamp(48px, 7vw, 88px);
}

.showcase-set > .wrap > .showcase {
  margin: var(--row-gap) 0;
}

/* The set's own padding already spaces the first and last rows off their
   neighbours; a margin here would double it. */
.showcase-set > :first-child > .showcase {
  margin-top: 0;
}

.showcase-set > :last-child > .showcase {
  margin-bottom: 0;
}

.closer {
  text-align: center;
}

.closer h2 {
  color: var(--teal);
}

/* Left unmeasured this paragraph runs the full 1152px container, which stranded
   "membership." alone on a second line on both closers. 58ch wraps it into two
   near-even lines instead (635/574 on pricing, 610/636 on the landing page) and
   sits next to the lede's 56ch rather than inventing a new measure.

   `text-wrap: pretty` is the guard for the next copy edit, not for this one:
   where it is supported the browser reflows to avoid a one-word last line. It is
   ignored elsewhere, so the measure above is what actually does the work. */
.closer p:not(.kicker) {
  color: var(--muted);
  margin: 14px auto 0;
  max-width: 58ch;
  text-wrap: pretty;
}

/* ── Privacy pledge panel ───────────────────────────────────────────────────
   The one loud claim on the site, so it carries the app's full active-surface
   treatment rather than a left accent bar: a solid 2px border in the claim's own
   colour plus the hard non-blurred offset shadow. That pairing — border and hard
   shadow in the same hue — is the app's signature, and a bar-plus-dots callout is
   the generic framework pattern it exists to avoid. Teal because this is the
   privacy claim, and teal is what the app spends on trust. */
.pledge {
  --tick: var(--teal);
  border: var(--bw-hair) solid var(--teal);
  box-shadow: 6px 6px 0 var(--shadow-teal);
  padding: clamp(24px, 4vw, 40px);
}

.pledge h2 {
  color: var(--teal);
}

.pledge .ticks {
  margin-top: 28px;
}

/* ── Ticked claim list ──────────────────────────────────────────────────────
   The pledge's promises and the pricing page's tier contents are the same
   object: a short line the panel is asserting, one per row. One definition, and
   the panel around it sets `--tick` to its own accent so the marks agree with
   the border they sit inside. */
.ticks {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: 1fr;
  column-gap: 40px;
}

/* Two columns once there is room: a dozen single-line claims stacked vertically
   read as a terms-and-conditions list, which is the opposite of the intent. */
@media (min-width: 820px) {
  .ticks--2col {
    grid-template-columns: 1fr 1fr;
  }
}

/* Hairline rules rather than gaps, matching the app's settings rows and the rule
   it draws under a section head. */
.ticks li {
  display: grid;
  grid-template-columns: 18px 1fr;
  align-items: start;
  gap: 14px;
  padding: 14px 0;
  border-top: var(--bw-hair) solid var(--border-w10);
  color: var(--body-dim);
  font-size: 16px;
  line-height: 26px;
  /* These lines are two to four words from wrapping, so a claim regularly lands
     with one word alone on a second line. Where it is supported this rebalances
     the break; where it is not, nothing changes. */
  text-wrap: pretty;
}

/* Drawn, not an icon font: two borders on a rotated box give the same 3px stroke
   as the lucide Check the app puts in an active toggle knob. */
.ticks li::before {
  content: '';
  width: 8px;
  height: 14px;
  margin: 5px 0 0 4px;
  border: solid var(--tick, var(--teal));
  border-width: 0 3px 3px 0;
  transform: rotate(45deg);
}

/* ── Pricing ──────────────────────────────────────────────────────
   The free tier is the loud card and the paid ones are the quiet ones, which is
   the inverse of how a pricing page usually ranks its own contents. That
   inversion is the argument the page is making, so it is carried in the styling
   rather than only in the copy.

   Free gets the app's full active-surface treatment — solid 2px border plus the
   hard non-blurred offset shadow, both in teal — the same pairing `.pledge` uses,
   because this is the same claim about trust. The planned tiers keep their shape but
   use a dashed border and no shadow. This treatment signals that those tiers are
   planned and stops them from reading as products available to buy. */
.tier {
  padding: clamp(24px, 3.6vw, 40px);
}

/* Narrower than the plans below it and centred. Two columns of ticks at the full
   1280px container turned the one card that matters into a spec sheet to be
   scanned; a single column at a readable measure is looked at instead. Being the
   only centred block on the page is what makes it the first thing seen. */
.tier--free {
  --tick: var(--teal);
  border: var(--bw-hair) solid var(--teal);
  box-shadow: 6px 6px 0 var(--shadow-teal);
  max-width: 720px;
  margin: 0 auto;
}

.tier--free h2 {
  color: var(--teal);
}

.tier--planned {
  --tick: var(--border-rose-50);
  border: var(--bw-hair) dashed var(--border-rose-50);
  box-shadow: none;
}

.tier--planned h3 {
  color: var(--rose);
  font-size: 1.5rem;
  font-family: 'Guanine', 'Anybody', system-ui, sans-serif;
}

/* Anybody, at display size, for the same reason the app keeps the clock in it:
   this is a numeral the eye reads at a glance rather than a word it parses. */
.price {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 10px;
  margin: 14px 0 0;
  font-family: 'Anybody', system-ui, sans-serif;
}

.price-figure {
  font-size: clamp(2.75rem, 6vw, 3.75rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--white);
}

.tier--free .price-figure {
  color: var(--teal);
}

.price-unit {
  font-size: 15px;
  font-weight: 700;
  line-height: 22px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The second way to pay for the same tier — the annual price, the founding
   price. Small and plain, so it never competes with the figure above it. */
.price-alt {
  margin: 10px 0 0;
  font-size: 14px;
  line-height: 22px;
  color: var(--muted);
}

.tier-lede {
  margin: 18px 0 0;
  font-size: 17px;
  line-height: 28px;
  color: var(--body-dim);
}

.tier .ticks {
  margin-top: 24px;
}

/* Two planned tiers, side by side once there is room. They are a fork rather than
   a ladder — nobody should buy both — so they are the same width and neither is
   marked as recommended. The top margin is the gap under the section heading,
   which now stands on its own with no paragraph between it and the cards. */
.tier-grid {
  display: grid;
  gap: 24px;
  grid-template-columns: 1fr;
  margin-top: 36px;
}

@media (min-width: 900px) {
  .tier-grid {
    grid-template-columns: 1fr 1fr;
  }
}

/* Body copy on a pricing page still has to be readable, and the reason-why
   sections here are three paragraphs deep. Same measure as the legal prose. */
.pricing-copy {
  max-width: 66ch;
}

.pricing-copy p {
  margin-bottom: 18px;
}

.pricing-copy .lede {
  max-width: none;
}

/* The landing page's section rhythm is sized for full-bleed device shots with
   large empty margins around them. This page is cards and paragraphs, and at the
   same spacing the free tier, the reason it is free, and the promise not to take
   it away drift apart into three unrelated statements. They are one argument
   read top to bottom, so the gaps close by roughly a third. */
.pricing section {
  padding: clamp(40px, 5.4vw, 68px) 0;
}

.pricing .hero {
  padding-bottom: clamp(24px, 3.4vw, 40px);
}

/* The free card is the hero's second half rather than the next section, so it
   keeps the pair together instead of sitting a full section gap below it — and
   the copy above it is centred on the same axis. A left-aligned headline over a
   centred card reads as two blocks that missed each other; centring both makes
   the pair one composition, and the left-aligned plans below it then read as the
   page moving on to a different subject. */
.pricing .free-tier {
  padding-top: clamp(16px, 2.2vw, 28px);
}

.pricing .hero .pricing-copy {
  max-width: 720px;
  margin: 0 auto;
  text-align: center;
}

.pricing .hero h1,
.pricing .hero .lede {
  max-width: none;
}

/* The closer is the one place the page should breathe again — it is a change of
   subject, not the next step of the argument. */
.pricing .closer {
  padding-top: clamp(56px, 8vw, 104px);
}

/* ── Prose (the legal pages) ────────────────────────────────────────────────
   Dense text on a full-bleed animated background is unreadable, so legal copy
   sits on the same glass surface the app uses for its in-app policy screen. */
.prose {
  max-width: var(--prose);
  padding: clamp(24px, 4vw, 48px);
  margin-bottom: 72px;
}

.prose h2 {
  font-size: 1.5rem;
  line-height: 1.3;
  margin: 44px 0 12px;
}

.prose h2:first-of-type {
  margin-top: 0;
}

.prose h3 {
  font-size: 1.0625rem;
  margin: 28px 0 8px;
  color: var(--marigold);
}

.prose p,
.prose li {
  font-size: 17px;
  line-height: 28px;
}

.prose ul,
.prose ol {
  margin: 0 0 1rem;
  padding-left: 22px;
}

.prose li {
  margin-bottom: 8px;
}

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

/* Terms and Privacy run several times longer than the FAQ, and at the shared
   prose size the column reads as a wall. One step down across the whole type
   stack keeps the proportions intact while fitting more of a clause on screen.
   The FAQ and delete-data keep the larger default. */
.legal .prose p,
.legal .prose li {
  font-size: 15px;
  line-height: 25px;
}

.legal .prose h2 {
  font-size: 1.3125rem;
}

.legal .prose h3 {
  font-size: 0.9375rem;
}

.legal .summary p {
  font-size: 15px;
  line-height: 24px;
}

.legal .note p {
  font-size: 14px;
  line-height: 23px;
}

.legal .page-title {
  font-size: clamp(2rem, 4vw, 2.625rem);
}

.legal .page-meta {
  font-size: 13px;
  line-height: 21px;
}

.page-title {
  margin: 26px 0 8px;
  color: var(--rose);
  font-size: clamp(2.25rem, 4.5vw, 3rem);
  line-height: 1.1;
}

.page-meta {
  color: var(--muted);
  font-size: 14px;
  line-height: 22px;
  margin: 0 0 40px;
}

/* Summary box at the top of a legal page — the honest one-paragraph version. */
.summary {
  border-left: var(--bw-accent) solid var(--marigold);
  padding: 20px 24px;
  margin-bottom: 36px;
  background: var(--glass-soft);
  border-radius: 0 var(--r-card) var(--r-card) 0;
}

.summary p {
  color: var(--white);
  font-size: 17px;
  line-height: 27px;
}

/* FAQ section jumps. Chip-shaped like the app's filter pills, but muted — this is
   navigation for people who already know which of the six they want, not a second
   headline competing with the page title. */
.faq-toc {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 40px;
}

.faq-toc a {
  padding: 7px 15px;
  border-radius: var(--r-pill);
  border: var(--bw-hair) solid var(--border-w20);
  color: var(--muted);
  font-size: 14px;
  line-height: 20px;
  text-decoration: none;
}

.faq-toc a:hover {
  border-color: var(--border-rose-50);
  color: var(--rose);
}

/* Callout for the things a reader must not miss — alarm reliability, the
   not-medical disclaimer. Rose, because in this system rose is emphasis ink. */
.note {
  border: var(--bw-hair) solid var(--border-rose-50);
  border-radius: var(--r-card);
  background: var(--chip-fill);
  padding: 18px 22px;
  margin: 24px 0;
}

.note p {
  color: var(--body-dim);
  font-size: 16px;
  line-height: 26px;
}

/* Unfilled value the publisher must replace before this site goes live. Loud on
   purpose: a placeholder that ships unnoticed into a legal document is the whole
   failure mode this styling exists to prevent. */
.todo {
  background: rgba(255, 223, 159, 0.16);
  border-bottom: 2px dashed var(--marigold);
  color: var(--marigold);
  padding: 0 3px;
  font-weight: 600;
  white-space: nowrap;
}

/* ── Dev log ────────────────────────────────────────────────────────────────
   Entries stack on one page rather than getting a file each. This site has no
   build step, so a page per post means hand-copying the head, header and footer
   for every entry and keeping a separate index in step with all of them — and a
   dev log is read as a stream anyway. Each entry keeps its own dated anchor, so
   one entry is still linkable on its own.

   The entries reuse `.prose` outright instead of restating its type scale. That
   is the same body copy the FAQ and the legal pages set, so an entry inherits
   every later refinement to it for free; only the two rules that position a
   standalone panel have to be undone here. */
/* `.page-title` only carries an 8px bottom margin because on every other page a
   `.page-meta` sits under it and supplies the 40px gap before the content. The
   blog has no subtitle, so the log has to stand itself off the title. Adjacent
   siblings collapse, so this stays 40px rather than doubling if a subtitle
   ever comes back. */
.log {
  display: grid;
  gap: 28px;
  max-width: var(--prose);
  margin-top: 40px;
  margin-bottom: 72px;
}

.log .prose {
  /* The column is set once, on `.log`. */
  max-width: none;
  /* The grid gap spaces the entries; `.prose`'s own trailing margin would double
     the last one and push the footer down. */
  margin-bottom: 0;
}

/* A deep link lands on the entry, not tight against the top of the window. */
.log-entry {
  scroll-margin-top: 24px;
}

/* The date is the permalink, so it is the one link in an entry that must not
   read as body copy: muted and undecorated, the same quiet treatment the footer
   links get, since neither is what the visitor came to read. */
.log-date {
  margin: 0 0 6px;
  font-size: 13px;
  line-height: 20px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
}

.log-date a {
  color: inherit;
  text-decoration: none;
}

.log-date a:hover {
  color: var(--white);
  text-decoration: underline;
}

/* `.prose h2:first-of-type` zeroes the top margin of an entry's heading, but the
   date line above it means the heading is no longer first. Restate it here. */
.log-title {
  margin-top: 0;
}

/* A screenshot inside an entry. The frame is not decoration: a raw capture has
   square corners, and square corners inside a 12px card read as something pasted
   in rather than part of it. The hairline does the rest of the work, because most
   captures of this app are darker than the card they land on and would otherwise
   have no edge at all.

   `width: 100%` rather than `max-width` so a capture narrower than the column is
   still framed to the column. The intrinsic `width`/`height` attributes on the
   <img> are what reserve its box before it loads. */
.log-figure {
  margin: 24px 0;
}

.log-figure img {
  display: block;
  width: 100%;
  border-radius: var(--r-card);
  border: var(--bw-hair) solid var(--border-w10);
}

/* A capture off the phone is portrait and roughly twice as tall as it is wide, so
   at the column's full width it is taller than the screen reading it — the entry
   stops being an entry and becomes a screenshot with text around it. This holds a
   tall capture to a phone-sized column and centres it, which is also closer to the
   size the thing is actually looked at.

   The caption centres with it. A left-aligned caption under a centred image reads
   as belonging to the paragraph below rather than to the picture above. */
.log-figure--portrait img {
  width: auto;
  max-width: 280px;
  margin-inline: auto;
}

.log-figure--portrait figcaption {
  text-align: center;
}

/* Not a <p>, so it does not inherit the prose size the paragraphs around it get.
   Sized and coloured like the fine print on the landing page instead — a caption
   is an aside about the picture, not the next sentence of the entry. */
.log-figure figcaption {
  margin-top: 10px;
  color: var(--muted);
  font-size: 14px;
  line-height: 22px;
}

/* No entries yet. Not styled as an entry — a dated-looking panel with no date is
   how a placeholder gets mistaken for the first post. It does borrow `.prose`'s
   padding, though: while it is the only panel on the page it sets the left edge
   the entries will later inherit, and while it sits under them its copy has to
   start on the same line theirs does. */
/* ── Footer ─────────────────────────────────────────────────────────────────
   The one place on the page that deliberately stops shouting. Everything above
   is neo-brutalist — 4px rules, rose display type, uppercase chips — and the
   footer answers with a hairline and small plain-case links in --muted. It is
   wayfinding and boilerplate: the visitor should be able to find the privacy
   policy without the privacy policy ever pulling their eye off the product. */
.site-footer {
  border-top: var(--bw-hair) solid var(--border-w10);
  padding: 32px 0 44px;
  margin-top: auto;
}

.footer-inner {
  display: flex;
  flex-wrap: wrap;
  gap: 16px var(--gutter);
  justify-content: space-between;
  align-items: flex-start;
}

/* Type set on the list, not the anchor. The <li> strut is what positions the
   line box, so a font-size on the <a> alone leaves the links riding ~7px below
   the legal column beside them — they are top-aligned, but the body's 1.6
   line-height is still doing the leading. Matching the legal block's 19px here
   puts the two columns on the same first baseline. */
.footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 13px;
  line-height: 19px;
}

.footer-links a {
  color: var(--muted);
  text-decoration: none;
}

/* Muted plain text is a weak link affordance, so the hover has to be the strong
   one — colour alone would not carry it. */
.footer-links a:hover {
  color: var(--white);
  text-decoration: underline;
}

/* The wayfinding links and the social row are one column, not two more items in
   the footer's flex row. At three columns the legal block loses the measure it
   needs and the icons end up marooned between two blocks of text.

   The gap is small because the icon boxes carry 11px of their own padding, so
   the space actually seen between the two rows is roughly 17px. */
.footer-nav {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* Brand marks, so these are filled paths rather than the stroked icons the rest
   of the site draws — there is no recognisable X or TikTok note in a 2px outline.
   Coloured and sized to sit with the links above them: this row is wayfinding
   like the rest of the footer, not a call to follow.

   The negative left margin is the icon box's own padding, cancelled, so the
   first glyph lines up with the column edge instead of sitting inset from it. */
.footer-social {
  display: flex;
  flex-wrap: wrap;
  list-style: none;
  margin: 0 0 0 -11px;
  padding: 0;
}

/* An 18px mark centred in a 40px box. The box is the tap target and is invisible
   until focused; shrinking it to the glyph would make three adjacent links a
   thumb cannot reliably hit apart. */
.footer-social a {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  color: var(--muted);
  text-decoration: none;
}

.footer-social a:hover {
  color: var(--white);
}

.footer-social svg {
  display: block;
  width: 18px;
  height: 18px;
}

/* --muted, not --muted-dim, and that matters more at 12px than it did at 14px:
   the AA floor is 4.5:1 either way, and #6B7280 lands around 4:1 over the
   animated background. The app bumped its own muted token from #9CA3AF to
   #B8C0CC in the launch-readiness audit for exactly this case. Shrink this text
   further only by shrinking it — never by dimming it. */
.footer-legal {
  color: var(--muted);
  font-size: 12px;
  line-height: 19px;
  max-width: 46ch;
}

.footer-legal a {
  color: var(--teal);
}

/* Sticky footer without a wrapper div. */
body {
  display: flex;
  flex-direction: column;
}

body > main {
  flex: 1 0 auto;
}

/* ── Motion ─────────────────────────────────────────────────────────────────
   Everything here is decoration, so everything here has to be optional. The rule
   the whole section is built on: the *resting* state is the visible state, and
   motion is only ever added on top by something that has proven it can also take
   it away.

   That is why `.reveal` hides nothing by default. reveal.js adds `.reveal-armed`
   to <html> only once it has confirmed it is running and that the visitor has not
   asked for reduced motion; the `opacity: 0` below is scoped under that class. A
   visitor with JavaScript off, or an IntersectionObserver that never fires, gets a
   fully painted page rather than a blank one. This is not hypothetical — the
   reduced-motion block near the top of this file forces
   `transition-duration: 0.01ms !important` globally, so a reveal built the usual
   way (hide in CSS, unhide in JS) would strand every section at zero opacity for
   exactly the people least able to work around it. */
.reveal {
  transition:
    opacity 0.6s cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 0.6s cubic-bezier(0.22, 0.61, 0.36, 1);
  transition-delay: calc(var(--d, 0) * 90ms);
}

html.reveal-armed .reveal {
  opacity: 0;
  transform: translateY(var(--reveal-lift));
}

html.reveal-armed .reveal.is-in {
  opacity: 1;
  transform: none;
}

/* The idle drift on the device frames. Animates the standalone `translate`
   property rather than `transform`, so it composes with `.phone--tilt`'s rotation
   instead of overwriting it — one element cannot hold two `transform` values, and
   an animation always wins. Browsers without independent transform properties
   simply skip the drift and keep the tilt. */
.float {
  animation: float 7s ease-in-out infinite;
}

/* Opposite phase on flipped rows, so two devices on screen at once never bob in
   lockstep. */
.showcase--flip .float {
  animation-delay: -3.5s;
}

/* What makes the three loops below safe under reduced motion, verified in the
   browser rather than assumed: the global reduce block collapses each one to a
   single 0.01ms iteration, and because `animation-fill-mode` is left at its
   default of `none`, a finished animation reverts to the element's *base* style —
   not to its 100% keyframe. So the rule to preserve is simply that the base style
   is the resting pose: the devices sit level, the bloom sits unscaled, and every
   dream-map focus overlay sits transparent over the unfiltered map.
   Adding `animation-fill-mode: forwards` to any of these would break that and
   strand the element on its last frame. Keyframes still return to their start so
   the loops are seamless when motion *is* allowed. */
@keyframes float {
  0% {
    translate: 0 0;
  }
  50% {
    translate: 0 -10px;
  }
  100% {
    translate: 0 0;
  }
}

/* Fade up, hold, fade back to the unfiltered map underneath. The gap at the end
   of each quarter is deliberate — returning to the whole map between focuses is
   what makes the next one read as a cluster lighting up rather than a slideshow
   cut. */
@keyframes mapFocus {
  0%,
  100% {
    opacity: 0;
  }
  4% {
    opacity: 1;
  }
  19% {
    opacity: 1;
  }
  23% {
    opacity: 0;
  }
}

@keyframes bloom {
  0% {
    opacity: 0.75;
    scale: 1;
  }
  50% {
    opacity: 1;
    scale: 1.06;
  }
  100% {
    opacity: 0.75;
    scale: 1;
  }
}

