/* ============================================================
   The homepage, below the fold.

   The hero's CSS stays inline in index.html because it is the
   critical path; everything here is what the visitor scrolls to.
   ============================================================ */

.section { position: relative; z-index: 1; }
.wrap { max-width: 1060px; margin: 0 auto; }
.eyebrow {
  font-size: 13px;
  font-weight: var(--weight-semibold);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin: 0 0 14px;
}
h2.lead {
  margin: 0 0 20px;
  font-size: clamp(28px, 3.6vw, 44px);
  line-height: 1.08;
  letter-spacing: -0.02em;
  font-weight: var(--weight-semibold);
  color: var(--ink-primary);
  text-wrap: balance;
}
.body-copy {
  margin: 0 0 16px;
  font-size: 16.5px;
  line-height: 1.6;
  color: var(--ink-soft);
  text-wrap: pretty;
}
.body-copy:last-child { margin-bottom: 0; }

.card {
  background: var(--surface-card);
  backdrop-filter: var(--surface-card-blur);
  -webkit-backdrop-filter: var(--surface-card-blur);
  border: 1px solid var(--ink-hair);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
}

/* ---- video ----
   A vertical YouTube Short since 2026-09-19, so the frame is 9:16 and sized by
   HEIGHT: tall enough to watch, short enough that the whole video fits on one
   screen, which a 9:16 frame sized by width never would on a desktop. Only a
   phone narrower than that height allows falls back to the width.

   --video-h is the one number. The width and the corner radius are both derived
   from it, so the frame cannot change shape in one and not the other. */
.video {
  padding: clamp(72px, 10vh, 120px) 24px 0;
  --video-h: clamp(360px, 78svh, 760px);
}
.video__frame {
  position: relative;
  width: min(100%, var(--video-h) * 9 / 16);
  aspect-ratio: 9 / 16;
  margin: 0 auto;
  /* A phone screen's corners, in proportion, because the cut IS a phone screen:
     the Dynamic Island is in the picture. Derived from the width rather than
     fixed, so a 279px phone frame and a 423px desktop one are the same shape.
     The 100vw stands in for the 100% a radius cannot use (it would resolve
     against the frame's own box); they differ only by a scrollbar, on a desktop
     window under ~430px wide, where it moves the radius by under 2px. */
  border-radius: calc(min(100vw - 48px, var(--video-h) * 9 / 16) * 0.11);
  /* ⚠️ NO `overflow: hidden`, although it looks like the obvious way to round
     the corners. Every child carries `border-radius: inherit` and rounds itself
     instead. With the frame clipping, iOS Safari composites it (the play
     circle's backdrop-filter is enough to force that) and then paints this
     box-shadow a second time inside the frame's own rectangle: a darker square
     behind both bottom corners, where the offset shadow is strongest. Seen on
     an iPhone 17 Pro simulator, iOS 26.5, 2026-09-19. Chrome never shows it,
     so it cannot be caught in a desktop browser. */
  /* The poster's own colours, top to bottom, sampled from it. The poster is
     lazy, so this is what shows for the moment before it decodes, and a frame
     that is already the right sky reads as the image arriving rather than as
     an empty box filling in. */
  background: linear-gradient(180deg, #947098 0%, #90667D 10%, #C8817D 20%,
    #C59C8B 30%, #A39284 50%, #626771 70%, #6C6D8D 100%);
  /* Same shadow as the one-list devices, so the two kinds of phone on this page
     sit at the same height off it; plus a hairline, because once it plays most
     of the video is the app's own pale lavender, nearly the page's colour, and
     would otherwise have no edge. */
  box-shadow: 0 0 0 1px var(--ink-hair), 0 16px 30px rgba(40, 30, 55, 0.20);
}
/* ⚠️ WHOLE PIXELS, at exactly 9:16. 78svh is almost never an integer, and a
   frame 312.83 x 556.14 had YouTube's player snap its video down to whole
   pixels and show a hairline of its own black background along the bottom and
   right edges while playing (iPhone 17 Pro simulator, 2026-09-19). A width in
   steps of 9px makes the height steps of 16px, so both come out whole.

   Gated because the declaration holds a var(): where round() is unknown it
   would be invalid at COMPUTED time, which falls back to `width: auto`, a frame
   the full width of the page, rather than to the declaration above it. */
@supports (width: round(down, 1px, 1px)) {
  .video__frame { width: round(down, min(100%, var(--video-h) * 9 / 16), 9px); }
}
.video__poster { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }
.video__embed {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border: 0;
  border-radius: inherit;
}
/* The whole poster is the control, not just the circle.

   ⚠️ The poster is the video's own cover, the image YouTube's player shows
   before it plays, and the circle sits dead centre because that is where
   YouTube's button lands on it. On an iPhone the first tap loads the player
   without starting it (see CLAUDE.md), so the visitor sees our cover turn into
   YouTube's: the same picture with the button in the same place. A different
   poster here would make that tap look like it swapped the video. */
.video__play {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: inherit;
}
/* The site's own ring, 3px outside, but around the frame's corners rather than
   the 4px the global `a:focus-visible` sets. (0,2,0) against its (0,1,1), which
   is what lets this win. */
.video__play:focus-visible { border-radius: inherit; }
.video__play span {
  width: 74px; height: 74px; border-radius: 50%;
  background: rgba(255,255,255,0.42);
  backdrop-filter: blur(12px) saturate(1.6);
  -webkit-backdrop-filter: blur(12px) saturate(1.6);
  border: 1px solid rgba(255,255,255,0.5);
  /* A lift the design's circle did not have, so the frost keeps an edge over
     the cover's lighter passages as well as the dark rock at its centre. */
  box-shadow: 0 6px 18px rgba(40, 30, 55, 0.14);
  display: flex; align-items: center; justify-content: center;
}
.video__play span::after {
  content: ""; width: 0; height: 0; margin-left: 5px;
  border-left: 20px solid rgba(42,36,52,0.9);
  border-top: 12px solid transparent;
  border-bottom: 12px solid transparent;
}
.video__play:hover span { background: rgba(255,255,255,0.56); }
.video__caption { text-align: center; margin: 16px 0 0; font-size: 14px; color: var(--ink-muted); }
/* Said BEFORE the press, because the press is the consent: nothing is fetched
   from YouTube until it happens. The aria-describedby on the play link reads
   the same line to a screen reader. */
.video__note { text-align: center; margin: 4px 0 0; font-size: 12.5px; color: var(--ink-muted); }

/* ---- the argument ----
   This is the section directly under the hero, and it starts on the hill's own
   flat colour and dissolves out of it, so scrolling off the hero reads as one
   continuous change of colour rather than a seam.

   --hill-foot and --hill-foot-fade are the same colour at alpha 1 and alpha 0,
   both written to :root by main.js on every hero render. Two variables rather
   than a fade to `transparent`, so nothing depends on how an engine
   interpolates a gradient's alpha. With scripts off both fall back to fully
   transparent and the section simply sits on the page background. */
.argument {
  /* Barely any top padding, because the break above the thesis IS the hero's
     own tail: a fifth of a viewport of flat hill colour that this section
     continues. Stacking a real padding on top of that just made the gap read as
     dead space, which is what it was. */
  padding: clamp(16px, 2.5vh, 40px) 24px clamp(40px, 6vh, 70px);
  text-align: center;
  background-image: linear-gradient(180deg,
    var(--hill-foot, transparent) 0%,
    var(--hill-foot-fade, transparent) 68%);
}
.argument__thesis {
  max-width: 860px; margin: 0 auto;
  font-size: clamp(26px, 3.5vw, 44px);
  line-height: 1.22;
  font-weight: var(--weight-semibold);
  letter-spacing: -0.02em;
  color: var(--ink-primary);
  text-wrap: balance;
}
.argument__thesis .muted { color: var(--ink-muted); }
.compare { max-width: 620px; margin: clamp(44px, 7vh, 72px) auto 0; display: flex; align-items: stretch;
  padding: clamp(18px,3vw,26px) clamp(10px,2vw,18px) clamp(16px,2.6vw,22px); }
.compare__col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 0 clamp(10px,2.4vw,22px); min-width: 0; }
.compare__rule { width: 1px; background: var(--ink-hair); margin: 2px 0; }
.compare__label { font-size: 12px; font-weight: var(--weight-semibold); letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--ink-muted); }
.compare svg { width: 100%; max-width: 210px; height: auto; display: block; overflow: visible; }
.compare__figure { font-size: 16px; font-weight: var(--weight-semibold); }
.compare__note { font-size: 13px; line-height: 1.35; color: var(--ink-muted); text-align: center; }
/* the sparklines draw themselves when the card first comes into view */
.compare .draw { stroke-dashoffset: var(--len); transition: stroke-dashoffset 1.1s cubic-bezier(0.4,0,0.2,1); }
.compare .late { opacity: 0; transition: opacity 0.4s ease 1.2s; }
.compare .early { opacity: 0; transition: opacity 0.5s ease 0.2s; }
.compare.is-in .draw { stroke-dashoffset: 0; }
.compare.is-in .late, .compare.is-in .early { opacity: 1; }
.compare .draw--b { transition-delay: 0.3s; }

/* ---- evidence ---- */
.evidence { padding: clamp(80px, 11vh, 140px) 24px; text-align: center; }
.evidence p {
  max-width: 720px; margin: 0 auto;
  font-size: clamp(19px, 2.3vw, 26px);
  line-height: 1.5;
  color: var(--ink-primary);
  text-wrap: pretty;
}
.evidence cite { display: block; margin-top: 22px; font-size: 12.5px; font-style: normal; color: var(--ink-muted); }

/* ---- one list ----
   THREE BEATS, ALTERNATING: copy left / shots right, then shots left / copy
   right, then back. Real 2x captures of the app, so there is no CSS phone
   chrome here to drift out of step with what the app actually looks like.

   ⚠️ The SHADOW is not part of the capture. The supplied mocks carry one that
   is already clipped at the canvas edge, so it could never be shown whole and
   read as a rectangle cut around each phone. The files are cropped to the
   device itself and the shadow is the drop-shadow filter below, which follows
   the rounded alpha of the frame and has nothing to clip against. */
.onelist { padding: clamp(90px, 13vh, 170px) var(--pad);
  --pad: 24px; --shot-w: 330px; --shot-gap: clamp(8px, 1.4vw, 18px);
  --shadow-y: 16px; --shadow-blur: 30px; }

/* Wider than the site's usual 1060 measure, deliberately: two 330px devices
   plus a readable measure do not fit in 1060.

   ⚠️ THE FLIP IS `row-reverse` AND NOTHING ELSE. Copy stays first in the DOM in
   every beat, so the stacked order below is text, shots, text, shots, and the
   reading order never depends on which side the shots are on. Do not reorder
   the markup to move a beat's shots; the visual order is this one line. */
.onelist__beat { --stagger: clamp(16px, 4vw, 46px);
  max-width: 1120px; margin: 0 auto;
  display: flex; gap: clamp(30px, 5vw, 72px);
  align-items: center; justify-content: center; }
.onelist__beat--flip { flex-direction: row-reverse; }
.onelist__beat + .onelist__beat { margin-top: clamp(72px, 10vw, 150px); }

.onelist__copy { flex: 1 1 340px; min-width: 300px; max-width: 430px; }

/* A device is --shot-w in all three beats and never a few pixels off because of
   what the copy beside it happened to need: the group is a FIXED COLUMN, always
   two shots wide. All three beats hold two today, so the width matches the
   content anyway; it is declared because the moment one of them holds one or
   three, the copy beside it grows or shrinks to suit and the three text columns
   stop lining up. Beat three did hold one until 2026-08-22, and its copy ran
   430 against the pairs' 378. Shots in such a beat are centred in the column:
   a 348px void between the copy and a right-aligned shot reads as something
   missing, where two even margins read as a column. */
.shots { flex: none; display: flex; gap: var(--shot-gap);
  width: calc(2 * var(--shot-w) + var(--shot-gap));
  align-items: flex-start; justify-content: center; }
.shot { flex: none; width: var(--shot-w); max-width: 100%; height: auto;
  filter: drop-shadow(0 var(--shadow-y) var(--shadow-blur) rgba(40, 30, 55, 0.20)); }

/* THE STAGGER ALTERNATES WITH THE SIDE. One device rides lower so a pair reads
   as two moments rather than as one specimen sheet, and the one that rides
   lower is always the OUTER one, away from the copy: second when the shots are
   on the right, first when they are on the left. Without the flip half, the
   stagger would lean the same way on both sides and the mirror would read as a
   mistake rather than as a rhythm.

   Margin, not a transform, so the beat's height accounts for the offset
   instead of the shot hanging out of it. Driven by --stagger rather than by
   overriding these rules later, because the flip selectors outweigh the plain
   ones and a cancel would have to out-specify them both. */
.shots .shot:nth-child(2) { margin-top: var(--stagger); }
.onelist__beat--flip .shots .shot:nth-child(1) { margin-top: var(--stagger); }
.onelist__beat--flip .shots .shot:nth-child(2) { margin-top: 0; }

/* THE WATCH BEAT HOLDS THREE, in the same fixed column as the phone pairs, so
   its copy measures what the others do. Three watches and two gaps fill
   exactly `2 * --shot-w + --shot-gap`, which is what --watch-w solves for.
   ⚠️ The column width is left alone: it reads --shot-w, so shrinking that to
   fit the watches would narrow the column and the copy would widen to suit.

   The stagger is the MIDDLE one riding lower, not the outer: three read as a
   set around a centre, and the outer-lower slope of the pairs becomes a
   staircase at three. The flip rule's first-child offset is cancelled here,
   same specificity and later, so it wins. */
.shots--watch { --watch-w: calc((2 * var(--shot-w) - var(--shot-gap)) / 3); }
.shots--watch .shot { width: var(--watch-w); }
.onelist__beat--flip .shots--watch .shot:nth-child(1) { margin-top: 0; }
.onelist__beat--flip .shots--watch .shot:nth-child(2) { margin-top: var(--stagger); }

/* ---- the sky section ----
   Restored 2026-08-22 in place of the journey. The copy is CENTRED OVER the
   sky rather than sitting beside it, which is why the scene wants `dodge` in
   main.js: the moon is shoved sideways out of the copy's band so a heading
   never lands on it.

   `pointer-events: none` on the copy is load-bearing. The whole section is
   tap-to-summon, and a block of text lying across the middle of it would
   otherwise swallow every tap that landed on a sentence. */
/* ⚠️ SIZED FROM BOTH ENDS, not from one. The section has to fit the copy ABOVE
   the crest and the scrubber BELOW the ridge, and at 92svh it fitted neither
   reliably: on a short viewport the panel sat over the hill with a camp showing
   through its blur (Yalin, 2026-08-22). `--sky-min` is written by main.js as
   `copy + air + the ridge's own drop + the panel + its inset`, so the floor is
   whichever is larger, a full screen or what the two ends actually need. It is
   a function of WIDTH alone, so it cannot chase its own tail. */
.skysec { position: relative; overflow: hidden;
  min-height: max(100svh, var(--sky-min, 0px)); }
.skysec__copy { position: relative; z-index: 1; max-width: 660px; margin: 0 auto;
  text-align: center; padding: clamp(70px,10vh,110px) 24px 0; pointer-events: none; }
.skysec .eyebrow, .skysec h2, .skysec p { color: var(--sky-ink-soft); text-shadow: var(--sky-shadow); }
.skysec h2 { color: var(--sky-ink); }
.skysec strong { color: var(--sky-ink); font-weight: var(--weight-semibold); }

/* ---- the hour scrubber ----
   A floating panel again, pinned to the bottom of the sky it drives. It spent
   2026-08-21 to 08-22 inside the journey card, as a flat row under the beats,
   because there was no sky section for it to float over; both the panel and
   the section came back together.

   ⚠️ It is the affordance that PROVES the sky is the visitor's own hour, which
   is the section's whole claim. Nothing else on the page can show you 3am. */
.scrub {
  position: absolute; left: 0; right: 0; bottom: clamp(18px,5vh,44px); z-index: 2;
  display: flex; justify-content: center; padding: 0 20px;
}
.scrub__panel {
  display: flex; flex-direction: column; gap: 10px;
  padding: 14px 20px; border-radius: 22px;
  background: var(--sky-chip-bg);
  backdrop-filter: blur(16px) saturate(1.6);
  -webkit-backdrop-filter: blur(16px) saturate(1.6);
  border: 1px solid var(--sky-chip-border);
  width: min(92vw, 520px); box-sizing: border-box;
  transition: background 1s ease, border-color 1s ease;
}
.scrub__head { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; }
.scrub__head span:first-child { font-size: 12.5px; font-weight: var(--weight-semibold); letter-spacing: 0.04em;
  color: var(--sky-ink-soft); white-space: nowrap; }
/* Fixed width, so the reading arriving does not shove the label sideways. */
.scrub__read { font-size: 12.5px; font-weight: var(--weight-medium); font-variant-numeric: tabular-nums;
  color: var(--sky-ink); white-space: nowrap; width: 158px; text-align: right; flex: none; }
.scrub__controls { display: flex; align-items: center; gap: 12px; }
.scrub input[type="range"] { flex: 1; min-width: 0; accent-color: var(--brand-sage); margin: 0; }
.scrub__now {
  border: none; background: rgba(123,164,137,0.2); color: var(--sky-ink);
  font-size: 12px; font-weight: var(--weight-semibold); padding: 6px 0; width: 96px; flex: none;
  border-radius: var(--radius-pill); cursor: pointer;
  opacity: 0; pointer-events: none; transition: opacity 0.25s ease;
}
.scrub__now.is-on { opacity: 1; pointer-events: auto; }

/* ---- widgets ---- */
.widgets { padding: clamp(90px,13vh,160px) 24px; }
.widgets__head { max-width: 760px; margin: 0 auto; text-align: center; }
.widgets__row { display: flex; gap: 18px; justify-content: center; align-items: flex-end;
  flex-wrap: wrap; margin-top: clamp(36px,6vh,56px); }
.widget {
  border-radius: 36px; overflow: hidden; border: 1px solid var(--ink-hair);
  box-shadow: 0 14px 34px rgba(40,30,55,0.12); position: relative; background: #EBE2F4;
}
.widget--s { width: 176px; height: 176px; }
.widget--m { width: 372px; max-width: 86vw; height: 176px; }
.widget--todo {
  width: 176px; height: 176px; border-radius: 36px; border: 1px solid var(--ink-hair);
  background: repeating-linear-gradient(45deg, rgba(60,50,70,0.05) 0 8px, transparent 8px 16px);
  display: flex; align-items: center; justify-content: center; text-align: center;
  padding: 16px; box-sizing: border-box;
  font-family: ui-monospace, monospace; font-size: 11px; color: var(--ink-muted); line-height: 1.5;
}

/* ---- what you get ---- */
.pricing { padding: clamp(60px,9vh,110px) 24px; }
.pricing__head { max-width: 760px; margin: 0 auto; text-align: center; }
.pricing__head h2 { margin-bottom: 34px; }
.tiers { display: flex; gap: 18px; justify-content: center; flex-wrap: wrap; max-width: 760px; margin: 0 auto; }
.tier { flex: 1 1 280px; max-width: 360px; padding: 26px 28px; }
.tier--plus { background: rgba(255,255,255,0.62); border-color: rgba(217,166,84,0.4); box-shadow: 0 10px 26px rgba(40,30,55,0.08); }
.tier__head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 18px; }
.tier__name { font-size: 19px; font-weight: var(--weight-semibold); color: var(--ink-primary); }
.tier__price { font-size: 14px; font-weight: var(--weight-medium); color: var(--ink-muted); }
.tier__badge { font-size: 13px; font-weight: var(--weight-semibold); color: #8A6A1E;
  background: rgba(217,166,84,0.16); border-radius: var(--radius-pill); padding: 5px 12px; }
.tier ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 11px;
  font-size: 15.5px; color: var(--ink-soft); }
.tier__foot { margin: 16px 0 0; font-size: 12px; color: var(--ink-muted); }
.pricing__policy { max-width: 620px; margin: clamp(36px,6vh,52px) auto 0; text-align: center; }
.pricing__punch { margin: 26px 0 0; font-size: clamp(20px,2.5vw,27px); line-height: 1.35;
  font-weight: var(--weight-semibold); letter-spacing: -0.01em; color: var(--ink-primary); }

/* ---- privacy ---- */
.privacy-note { padding: clamp(70px,10vh,130px) 24px; text-align: center; }
.privacy-note .wrap { max-width: 620px; }

/* ---- closing ----
   ⚠️ The copy is TOP-ALIGNED, not centred, and that is what lets the ridge be
   placed under it. Centred, the block's bottom sits past the middle of the
   section by construction, so on a tall copy (a phone, where the heading, the
   body, the button, the note and the waitlist all stack) it ran straight
   through the hill and the camps. Top-aligned, main.js can measure where the
   copy actually ends and put the crest below it: see the frac callback there,
   which keeps the design's 0.66 wherever the copy fits inside it. */
/* ⚠️ The floor is 92svh but the real height often comes from --closing-min,
   which main.js sets to "where the copy ends, plus a band deep enough for the
   whole ridge". Without it the section stops at 92svh, the crest is pushed down
   to clear the copy, and what is left underneath is shallower than the ridge's
   own drop: the footer then cuts the hill and the first camps in half, which is
   what a phone showed. */
.closing {
  position: relative; overflow: hidden;
  min-height: max(92svh, var(--closing-min, 0px));
  display: flex; align-items: flex-start; justify-content: center;
}
.closing__copy { position: relative; z-index: 1; max-width: 600px; margin: 0 auto;
  text-align: center; padding: clamp(72px, 11vh, 120px) 24px clamp(28px, 5vh, 48px);
  color: var(--sky-ink); }
.closing h2 {
  margin: 0 0 16px; font-size: clamp(32px,4.4vw,54px); line-height: 1.05;
  letter-spacing: -0.02em; font-weight: var(--weight-semibold);
  color: var(--sky-ink); text-shadow: var(--sky-shadow);
}
.closing__body { margin: 0 auto; max-width: 480px; font-size: 16.5px; line-height: 1.6;
  color: var(--sky-ink-soft); text-wrap: pretty; text-shadow: var(--sky-shadow); }
.closing__note { margin-top: 14px; font-size: 13.5px; color: var(--sky-ink-soft); }

/* ---- the beta form ----
   Was a waitlist, and the class names are still the waitlist's. Since
   2026-08-22 the address is how you get INTO the beta: the TestFlight invite is
   sent by email, so this field is the only door and the hero's CTA scrolls to
   it. Hence one field, one button, and no "not ready?" framing above it. */
.waitlist { margin-top: 34px; }
.waitlist__form { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }
.waitlist input[type="email"] {
  width: min(260px, 64vw); padding: 13px 18px; border-radius: var(--radius-pill);
  border: 1px solid var(--sky-chip-border); background: rgba(255,255,255,0.85);
  backdrop-filter: blur(14px) saturate(1.6);
  -webkit-backdrop-filter: blur(14px) saturate(1.6);
  font-size: 14.5px; color: var(--ink-primary); outline: none;
}
/* The same green and the same shadow as `.btn--primary`, because this button and
   the hero's are now one control seen twice. It keeps its own smaller padding
   rather than taking the `.btn` class: `.btn` is 16px type on 15px of padding,
   which stands taller than the field beside it.
   ⚠️ The transparent border is not decoration. The field carries a 1px border
   and the button does not, so without it the pair sits 2px out of true. */
.waitlist button {
  border: 1px solid transparent; cursor: pointer;
  background: var(--cta-green); color: #FFFFFF;
  box-shadow: 0 8px 22px rgba(40,30,55,0.20);
  font-size: 14.5px; font-weight: var(--weight-semibold); padding: 13px 24px; border-radius: var(--radius-pill);
}
.waitlist button:hover { background: var(--cta-green-hover); }
.waitlist button[disabled] { opacity: 0.6; cursor: default; }
/* The requirement, under the field. `.closing__copy` is centred, so this only
   needs its own spacing. */
.waitlist .closing__note { margin-top: 14px; }

/* THE FORM STACKS AT 481px, AND 481 IS MEASURED, NOT A ROUND NUMBER.
   The pair needs the field's 260, the 8px gap and the button at its max-content:
   about 433.5, since the button's 165 is a rounded reading of ~165.5.
   `.closing__copy` is `min(600px, W)` less 48px of padding, so the row survives
   while `W - 48 >= 433.5`, which is **W >= 482**.

   ⚠️ Measured at the boundary rather than derived, and the arithmetic alone gets
   it wrong by a pixel: 482 shares a row, 481 already wraps. A query at 480 would
   leave 481 wrapping WITHOUT the full-width treatment, which is a one-pixel band
   showing the old narrow button under a narrow field. Pinned at 481 the two
   treatments meet exactly and there is no such band.

   ⚠️ The FIELD goes full width too, not just the button, and Yalin asked only
   for the button (2026-08-22). Widening the button alone puts a 344px control
   under a 251px one, which is the same imbalance the other way up: a submit
   wider than the input it submits. Full width on both is the stacked pair he
   was after. Narrowing the field back is one line if he wants it.

   ⚠️ `flex-direction: column`, not a reliance on wrap. Wrapping gets the same
   two rows, but the button would then have to GROW into its line, and
   `flex-grow` is resolved per line before `justify-content`, so the same
   declaration would stretch the button on a desktop row as well. Column sidesteps
   it: `align-items` is stretch by default, so the button fills without being
   told to, and only the field needs its `min(260px, 64vw)` overridden. */
@media (max-width: 481px) {
  .waitlist__form { flex-direction: column; }
  .waitlist input[type="email"] { width: 100%; }
}
/* Visually hidden and out of the tab order, but a bot filling the form by
   parsing its fields will still fill it. */
.waitlist__pot {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}
.waitlist__msg { margin: 12px 0 0; font-size: 14.5px; font-weight: var(--weight-medium);
  color: var(--sky-ink); min-height: 1.4em; }
.waitlist__msg--error { color: #FFD9D2; }

/* THE BEATS STACK AT 1120px, AND 1120 IS ARITHMETIC. A beat holds 678px of
   shots (2x330 plus the gap) and will not shrink them, so the copy takes the
   whole squeeze: at a viewport W it gets `min(1120, W-48) - gap - 678`, which
   is 339px at 1121 and 300px at 1080. 339 is about the narrowest measure this
   body size reads well at, so the beats go to one column just below it.

   Stacked, `align-items: stretch` is what lets .shots span the beat so the
   snap padding below has a full width to halve. The copy is centred as a
   block and stays left-aligned as text. */
@media (max-width: 1120px) {
  .onelist__beat, .onelist__beat--flip { flex-direction: column; align-items: stretch; }
  .onelist__copy { flex: none; max-width: 560px; margin-inline: auto; }
  .shots { width: auto; justify-content: center; margin-top: 10px; }
}

/* A STACKED PAIR BECOMES A NATIVE SNAP CAROUSEL AT 760px, and 760 is
   arithmetic too: two 330s plus their gap plus the section's 48px of padding
   is 726, so below about that they no longer fit and would have to shrink past
   being readable. Stacking them vertically instead would make this section
   text, shot, shot, text, shot, shot, text, shot, which is not the alternation
   the layout is for, and about 4,500px tall on a phone.

   No JS, unlike the widget carousel below: there is no marquee here to cancel,
   so the browser owns the whole gesture and momentum, rubber-banding and
   snap-to-centre are all free and all feel native.

   ⚠️ `justify-content: flex-start`, NOT the centre it inherits. A centred flex
   container whose content overflows pushes the first item off the leading edge
   where scrolling cannot reach it.

   ⚠️ The padding is what lets the FIRST and LAST rest centred rather than
   pinned to an edge: half the leftover. A beat holding one shot would simply
   have nothing to scroll, so it needs no case of its own.

   ⚠️ THE SCROLLPORT MUST BLEED TO THE SCREEN EDGE, and it must clear the
   shadow on all four sides. `overflow-x: auto` makes this a scroll container,
   and a scroll container CLIPS: `overflow-y: visible` computes to `auto` the
   moment the other axis is not visible, so the box clips in BOTH axes. The
   shot's own drop shadow reaches 30px sideways, 14 above and 46 below, all of
   which fell outside a box that was exactly the shot's height and the
   section's content width. Two phones' worth of soft shadow, cut off square,
   read as a dark rectangle sitting behind the carousel (Yalin, 2026-08-22),
   and the section's 24px of padding left a dead margin the neighbouring device
   could never reach into. Bleeding fixes the sides, since a shadow clipped at
   the screen edge is just content going off screen; the block padding fixes
   the rest. */
@media (max-width: 760px) {
  .onelist__beat { --stagger: 0px; }
  .shots {
    --strip-shot: min(330px, 76vw);
    /* How far the drop shadow actually reaches, derived from the filter rather
       than typed, so the two cannot drift apart.

       ⚠️ IT REACHES FURTHER THAN THE BLUR RADIUS. A CSS blur radius is TWICE
       the Gaussian's standard deviation, and a Gaussian is not spent until
       about 3 sigma, so the real tail runs to ~1.5x the radius. Sized to the
       radius itself, the clip lands where the shadow is faint but not zero and
       leaves a hairline across the top and bottom of the carousel: the dark
       band again, only thin enough to read as a border (Yalin, 2026-08-22).
       2x is 1.5x plus headroom, since the exact tail is implementation
       defined and this cannot be tested in every engine.

       The envelope costs nothing to overshoot: the padding and the negative
       margins cancel exactly, `(10 - t) + (h + t + b) - b`, so the outer
       height is `h + 10` whatever the reach is, and nothing on the page
       moves. */
    --shadow-reach: calc(var(--shadow-blur) * 2);
    --reach-top: calc(var(--shadow-reach) - var(--shadow-y));
    --reach-bottom: calc(var(--shadow-reach) + var(--shadow-y));

    justify-content: flex-start;
    overflow-x: auto; overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory; scrollbar-width: none;

    /* FULL BLEED: the scrollport is the whole screen, not the section's
       content box. Sized off the parent rather than 100vw so a scrollbar can
       never make it overflow the page. */
    width: calc(100% + 2 * var(--pad));
    margin-inline: calc(-1 * var(--pad));
    padding-inline: calc((100% + 2 * var(--pad) - var(--strip-shot)) / 2);

    /* Room for the shadow, taken straight back out of the layout, so the box
       grows but nothing moves. */
    padding-block: var(--reach-top) var(--reach-bottom);
    margin-block: calc(10px - var(--reach-top)) calc(-1 * var(--reach-bottom));
  }
  .shots::-webkit-scrollbar { display: none; }
  .shot { width: var(--strip-shot); scroll-snap-align: center; }
  /* A watch rests smaller than a phone in the strip, so it never reads as the
     bigger device, and more of its neighbours peek in. --watch-w follows, or
     the desktop rule above would still size it. */
  .shots--watch { --strip-shot: min(250px, 62vw); --watch-w: var(--strip-shot); }
}

@media (max-width: 700px) {
  .compare { flex-direction: column; gap: 22px; }
  .compare__rule { width: auto; height: 1px; margin: 0 24px; }
}

/* ---- the widget gallery ----------------------------------
   Tiles are recreations at the app's own point sizes. The sky tiles run the
   real renderer, so they carry the visitor's hour like everything else. */

.carousel {
  position: relative;
  overflow: hidden;
  margin-top: clamp(36px, 6vh, 56px);
  /* full-bleed, so the strip runs off both edges rather than ending in a gap */
  width: 100vw;
  margin-left: calc(50% - 50vw);
  cursor: grab;
  /* let a vertical swipe still scroll the page */
  touch-action: pan-y;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}
.carousel:active { cursor: grabbing; }
/* Off above 700px on purpose. Up there the strip drifts, and the drift is
   itself the cue that there are more tiles; dots would be saying it twice. */
.cdots { display: none; }
/* Tiles centre on one line whatever their family: an inline accessory is 24pt
   tall and a large widget is 354, and bottom-aligning them made the strip read
   as a bar chart. */
.carousel__track { display: flex; align-items: center; gap: 22px; width: max-content; will-change: transform; }

.wcell { margin: 0; position: relative; flex: none; padding-bottom: 34px; }
.wcell__cap { position: absolute; left: 2px; right: 0; bottom: 0; }
.wcell__cap { display: flex; flex-direction: column; gap: 1px; }
.wcell__name { font-size: 13px; font-weight: var(--weight-medium); color: var(--ink-soft); }
.wcell__fam { font-size: 12px; color: var(--ink-muted); }

.wtile {
  position: relative; overflow: hidden; flex: none;
  border-radius: 22px;
  background: linear-gradient(180deg, #EBE2F4 0%, #EADFEF 100%);
  border: 1px solid var(--ink-hair);
  box-shadow: 0 14px 34px rgba(40, 30, 55, 0.12);
  user-select: none;
}
.wtile__sky { position: absolute; inset: 0; }
/* The extra-large capture keeps its own rounded corners, transparent outside a
   ~27px radius at 2x, where the mediums were cut square. At the shared 22px the
   tile's pale ground would show in all four corners, so this one tile follows
   the capture. */
.wtile--extraLarge { border-radius: 27px; }
/* Device stills fill the tile; the tile's own radius rounds them, so the
   crops themselves can stay square-cornered. */
.wtile__shot { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.wtile__pad { position: relative; padding: 14px; height: 100%; box-sizing: border-box;
  display: flex; flex-direction: column; }
.wtile__eyebrow { margin: 0 0 8px; font-size: 10px; font-weight: var(--weight-semibold);
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-muted); }

.wtile__tally .wtile__big { margin: 8px 0 0; font-size: 40px; line-height: 1;
  font-weight: var(--weight-semibold); font-variant-numeric: tabular-nums; color: var(--ink-primary); }
.wtile__tally .wtile__sub { margin: 3px 0 0; font-size: 13px; font-weight: var(--weight-medium); color: var(--ink-soft); }

.wtile__nexthead { display: flex; align-items: baseline; justify-content: space-between; gap: 6px; }
.wtile__nexthead .wtile__eyebrow { margin-bottom: 0; }
.wtile__disc { width: 34px; height: 34px; border-radius: 50%; background: rgba(255,255,255,0.7);
  border: 1px solid rgba(60,50,70,0.08); display: flex; align-items: center; justify-content: center;
  font-size: 18px; margin-top: 10px; }
.wtile__name { margin: 9px 0 0; font-size: 15px; font-weight: var(--weight-medium); color: var(--ink-primary); }
.wtile__start { margin-top: auto; align-self: flex-start; font-size: 11px; font-weight: var(--weight-semibold);
  color: var(--ink-primary); background: var(--ink-pill-tint); border: 1px solid var(--ink-hair);
  border-radius: var(--radius-pill); padding: 4px 9px; }

/* ---- a habit row at widget scale, the app's WidgetRow ----
   Restored 2026-08-21. These went out with the tiles that used them when the
   carousel dropped from fifteen to eight, but `wrow__chip` did NOT: the Next up
   tile still emits one, so "Due today" had been rendering as bare text jammed
   against its eyebrow ever since.

   ⚠️ Only the chip renders today. The rest is here for the same reason
   `spec.sky` is still wired into buildGallery: `row()` in widgets.js is what a
   rebuilt For today or One habit's climb tile would be built from, and a helper
   whose styles have been deleted does not "still work". Delete both or neither,
   which is exactly the half-and-half that caused this. */
.wrow { position: relative; overflow: hidden; display: flex; align-items: center; gap: 9px;
  background: rgba(255,255,255,0.55);
  backdrop-filter: var(--surface-card-blur); -webkit-backdrop-filter: var(--surface-card-blur);
  border-radius: 14px; padding: 8px 10px; box-shadow: 0 4px 8px rgba(40,30,55,0.07); }
.wrow__fill { position: absolute; top: 0; left: 0; bottom: 0; background: rgba(46,154,160,0.30); }
.wrow__fill::after { content: ""; position: absolute; top: 0; right: 0; bottom: 0; width: 1.5px; background: rgba(255,255,255,0.4); }
.wrow > *:not(.wrow__fill) { position: relative; }
.wrow__emoji { width: 26px; height: 26px; border-radius: 50%; background: rgba(255,255,255,0.7);
  border: 1px solid rgba(60,50,70,0.08); display: flex; align-items: center; justify-content: center;
  font-size: 14px; flex: none; }
.wrow__name { flex: 1; font-size: 13px; font-weight: var(--weight-medium); color: var(--ink-primary);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wrow__chip { display: flex; align-items: center; gap: 5px; font-size: 10px;
  font-weight: var(--weight-medium); color: var(--ink-soft); white-space: nowrap; }
.wrow__chip::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: var(--dot, var(--chip-today)); }
.wrow__timer { font-size: 12px; font-weight: var(--weight-semibold); font-variant-numeric: tabular-nums;
  color: var(--ink-primary); background: rgba(255,255,255,0.5); border-radius: var(--radius-pill); padding: 2px 7px; }

/* The Lock Screen renders every accessory in a MONOCHROME VIBRANT material:
   the system takes the alpha and throws the hue away. None of Loma's colour
   survives, so these tiles do not pretend it does. */
.wtile--lock { background: #16121C; border-color: rgba(255,255,255,0.14); box-shadow: none;
  color: rgba(255,255,255,0.92); display: flex; align-items: center; justify-content: center; }
.wtile--lock > * { filter: grayscale(1) brightness(1.7); }
.wlock__inline { margin: 0; font-size: 13px; font-weight: var(--weight-medium); white-space: nowrap; }
.wlock__rect { padding: 0 12px; width: 100%; box-sizing: border-box; }
.wlock__lead { margin: 0 0 4px; font-size: 12px; font-weight: var(--weight-semibold); }
.wlock__line { margin: 0; font-size: 11px; opacity: 0.85; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.wisland { background: #0B0B0D; border-radius: var(--radius-pill); height: 100%; box-sizing: border-box;
  display: flex; align-items: center; justify-content: space-between; padding: 0 14px; color: #fff; }
.wisland__emoji { font-size: 15px; }
.wisland__time { font-size: 14px; font-weight: var(--weight-semibold); font-variant-numeric: tabular-nums; }

@media (prefers-reduced-motion: reduce) {
  /* no drift: it becomes an ordinary horizontal scroller */
  .carousel { overflow-x: auto; cursor: default; -webkit-overflow-scrolling: touch; }
  .carousel__track { width: auto; }
}

/* ---- the strip on a phone: a snap carousel, not a marquee ----
   Yalin, 2026-08-21. The other half of this switch is in `main.js`, under the
   same 700px; read the note there for why the marquee is wrong at this width
   and right above it. Short version: a medium tile is 338px against a 390px
   screen, so a strip that never stops clips a widget at both edges the whole
   time and never holds one still.

   Everything here is the browser's own scroller. Momentum, rubber-banding and
   snap-to-centre are all free and all feel native, and the marquee's pointer
   handlers come off in JS so they cannot fight it. */
@media (max-width: 700px) {
  .carousel {
    /* x scrolls, y stays clipped. Naming only overflow-x leaves y at hidden,
       which is the pair that gives a horizontal scroller and no vertical one. */
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    cursor: default;
    /* the browser owns the horizontal gesture now, not the drag handler */
    touch-action: pan-x pan-y;
    /* ⚠️ No edge fade here. A centred medium tile reaches within 26px of both
       edges of a 390px screen, so the 6% ramp would sit ON the tile and dim
       the very thing the visitor stopped to look at. On a desktop the ramp
       falls in the gaps between tiles, which is what it is for. */
    -webkit-mask-image: none;
    mask-image: none;
    /* the snap points say where you are; a scrollbar under them is noise */
    scrollbar-width: none;
  }
  .carousel::-webkit-scrollbar { display: none; }
  /* Half the widest tile (338), so the first tile is already centred at rest
     and every tile after it can reach the centre. `max` keeps the padding from
     going negative below 338px, which would drop the declaration outright. */
  .carousel__track { padding-inline: max(0px, calc(50vw - 169px)); }
  .wcell { scroll-snap-align: center; }
  /* The second lap only exists to make the marquee's loop seamless. There is
     no loop here, so scrolling the same eight tiles twice is just a longer
     strip that repeats itself. */
  .wcell--lap2 { display: none; }

  /* ⚠️ The dots exist because nothing else here says there is more to the
     right. The marquee's drift does that job above 700px; a stopped snap
     carousel with one tile centred and its neighbours off screen looks like
     one tile (Yalin, 2026-08-22). Same treatment the journey's beat dots had:
     6px, and the current one stretches to a pill rather than just recolouring,
     so the position reads at a glance and not only on close inspection. */
  .cdots { display: flex; justify-content: center; gap: 7px; margin-top: 20px; }
  .cdots i {
    height: 6px; width: 6px; border-radius: var(--radius-pill);
    background: rgba(60, 50, 80, 0.25);
    transition: width 0.3s ease, background 0.3s ease;
  }
  .cdots i.is-on { width: 18px; background: var(--ink-primary); }
}

@media (max-width: 700px) and (prefers-reduced-motion: reduce) {
  .cdots i { transition: none; }
}
