/* ══ sota.bot — onboarding ══════════════════════════════════════════════════════
   The flow a reader lands in after pressing Get Started.

   It is the Wabi onboarding prototype's flow, one screen for one screen, wearing
   this brand instead of that one: Schibsted Grotesk for both of its faces, this
   paper and this ink, one accent, and the two-eyed square where the other mark was.

   ⚠ MOBILE IS THE PRODUCT AND DESKTOP IS THE PREVIEW. Every measurement below was
   chosen against a 375-wide screen and then checked at 320 and 430; the desktop
   frame is the same screen inside `img/phone.webp` and gets no separate design.

   ⚠ A CSS COMMENT CANNOT CONTAIN THE COMMENT-CLOSE TOKEN, NOT EVEN QUOTED. The
   landing page shipped with no shadow at all for a round because a note quoted it:
   error recovery skips to the next semicolon and takes the declaration with it.
   Name the token, never type it.
   ────────────────────────────────────────────────────────────────────────────── */

/* ⚠ DECLARED AS A RANGE. One variable file carries 400-900, and without the range
   the browser synthesises a fake bold from the 400 instance — smeared at exactly the
   size it matters most. It is also what lets the weight tracker's re-skin ANIMATE
   its weight, which is the whole demonstration on screen two. */
@font-face {
  font-family: 'Schibsted';
  src: url('/fonts/Schibsted.woff2') format('woff2');
  font-weight: 400 900;
  font-style: normal;
  font-display: block;
}

:root {
  --paper:  #F5F4F1;
  --ink:    #111310;
  --muted:  #6B6F69;
  --hair:   rgba(17, 19, 16, 0.12);
  --hair-2: rgba(17, 19, 16, 0.07);
  /* ⚠ CHATGPT'S BLUE, and the two companions are not decoration. White on the pure
     brand blue measures 3.6:1 — under AA for the 15px semibold the lead card sets —
     so the accent is a touch deeper at 5.0:1, and small type on the pale tint gets
     its own darker ink at 6.7:1 rather than being set in the accent at 4.4. */
  --accent: #0A65F5;
  --accent-soft: #E9F1FE;
  --accent-ink: #084BB8;

  /* the app's own ground. ⚠ A SCREEN IS EMISSIVE AND A PAGE IS NOT, so the two
     cannot be the same colour — this is the landing phone's own white, and the
     card grey is the landing phone's own `#F1F2F0`. */
  --bg:   #FFFFFF;
  --fill: #F1F2F0;
  --ok:   #3F7A5C;

  --r:    14px;
  --r-lg: 20px;

  /* ⚠ THE GUTTER IS ONE NUMBER. The prototype carried 26px of screen padding and
     then a second inset on the chips; two gutters is how a list ends up visibly
     narrower than the header above it with nothing in either rule saying why. */
  --gut: 22px;
}

* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: 'Schibsted', system-ui, -apple-system, sans-serif;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  -webkit-tap-highlight-color: transparent;
  overflow: hidden;
}
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
input  { font: inherit; color: inherit; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }

/* ══ THE MARK (desktop only) ════════════════════════════════════════════════════ */
.mark {
  position: fixed; top: 26px; left: 30px; z-index: 3;
  display: none; align-items: center; gap: 9px;
  text-decoration: none; color: var(--ink);
  font-size: 19px; font-weight: 600; letter-spacing: -0.014em; line-height: 1;
}
.mk-logo { display: block; width: 22px; height: 22px; flex: none; }
.mk-logo svg { display: block; width: 100%; height: 100%; fill: var(--ink); }
.mk-eyes { fill: var(--paper); transition: transform 700ms cubic-bezier(0.42, 0, 0.12, 1); }
.mark:hover .mk-eyes { transform: translateX(4px); }
.mk-t { color: var(--muted); }

/* ══ THE DEVICE ═════════════════════════════════════════════════════════════════
   ⚠ THE PHONE FRAME IS DESKTOP-ONLY MARKUP THAT IS ALWAYS IN THE DOM. It is hidden
   rather than removed so nothing has to be rebuilt on a rotate, and its `img` is
   `display:none` under 901px so the 760x1562 bitmap is never fetched on a phone. */
.deck { height: 100%; display: block; }
.phone { position: relative; }
.frame { display: none; }

/* ── mobile: the app IS the viewport ─────────────────────────────────────── */
.app {
  position: fixed; inset: 0;
  background: var(--bg);
  color: var(--ink);
  display: flex; flex-direction: column;
  overflow: hidden;
  /* ⚠ `dvh`, NEVER `vh`. A mobile browser's own chrome takes a hundred pixels off
     the bottom that nothing can scroll back, and this flow ends in a button. */
  height: 100dvh;
  /* ⚠ THE SAFE AREA IS PADDING ON THE APP, NOT ON EVERY SCREEN. Written per screen
     it has to be repeated ten times and is wrong the first time one is added. */
  padding-top: max(10px, env(safe-area-inset-top));
  padding-bottom: env(safe-area-inset-bottom);
  /* ⚠ THE APP IS A CONTAINER, AND EVERY TYPE SIZE IN IT IS A `cqw` OF THIS BOX
     RATHER THAN A `vw` OF THE WINDOW. On a phone the two are the same number and it
     looks like a free choice; inside the desktop frame the screen is ~352px in a
     1440px window, so `7.1vw` resolved to the clamp's 30px CEILING in a column
     narrower than a phone — the headline that sets two lines on a 390 device set
     THREE inside the frame, and the desk view stopped being a preview of the phone
     view. The container is the glass, exactly as the landing page's screen is.
     ⚠ AND IT CANNOT BE READ BY THE ELEMENT THAT DECLARES IT: `container-type` sets
     up a container for an element's DESCENDANTS, never for itself. Nothing on this
     rule may use `cqw`. */
  container-type: inline-size;
}
.foot { display: none; }

/* ── desktop: a device standing on paper ──────────────────────────────────── */
@media (min-width: 901px) {
  .mark { display: inline-flex; }
  .deck {
    display: grid; place-items: center;
    padding: 30px 30px 26px;
  }
  .phone {
    /* height-led, and the width follows from the frame's OWN aspect (760/1562)
       rather than from a second number that has to be kept in step with it */
    --ph-h: min(88vh, 812px);
    height: var(--ph-h);
    width: calc(var(--ph-h) * 0.48655);
    flex: none;
    /* ⚠ A `filter`, NEVER A `box-shadow`. A box-shadow is the shape of the BOX and
       this phone's box is a rectangle while the thing in it has rounded corners and
       a transparent surround. The values are the landing page's own. */
    filter: drop-shadow(rgba(0, 0, 0, 0.086) 0 14px 24px)
            drop-shadow(rgba(0, 0, 0, 0.06) 0 4px 8px);
  }
  .frame {
    display: block; position: relative; z-index: 2;
    width: 100%; height: 100%; pointer-events: none;
  }
  .app {
    position: absolute; z-index: 1; inset: 0.9% 3.3% 1.0% 3.25%;
    height: auto;
    border-radius: 13.5% / 6.4%;
    /* ⚠ IT HAS TO CLEAR THE DYNAMIC ISLAND, which is painted into the frame image and
       sits over the top of the glass — on a phone the safe-area inset does this job
       and there is nothing to do.

       ⚠ AND THE NUMBER IS READ OFF THE BITMAP, NOT GUESSED. Scanning img/phone.webp
       (760×1562) for the dark pill: it runs 0.58% → 6.91% of the frame's height and
       33.3% → 66.5% of its width. The first pass here used 5.5% "measured at ~4.6%",
       which is BELOW the island's real bottom edge — so the header row began level
       with the pill rather than under it, and the mark sat in the island's own band.
       It clears horizontally (the mark is at the left, the island is centred), which
       is exactly why it read as vaguely off rather than as broken.

       The app is inset 0.9% from the phone's top, so the island's bottom is
       (6.91 − 0.9)% = 6.01% of the phone's height below the app's own top edge. The
       12px is the gap under it. */
    --island: calc(var(--ph-h) * 0.0601);
    padding-top: calc(var(--island) + 12px);
    padding-bottom: 0;
  }
  .foot {
    position: fixed; left: 0; right: 0; bottom: 14px;
    display: flex; justify-content: center; gap: 8px;
    font-size: 12px; color: var(--muted); letter-spacing: -0.002em;
    pointer-events: none;
  }
  .foot-dot { opacity: 0.45; }
}

/* ══ THE BAR ════════════════════════════════════════════════════════════════════
   ⚠ IT IS THE ONE FIXED THING AND IT NEVER MOVES. Every screen scrolls under it, so
   the back button is in the same place on all ten — a control that changes position
   between two screens of one flow is a control somebody has to look for. */
/* ⚠ THREE COLUMNS, AND THE MIDDLE ONE IS `auto`. `1fr auto 1fr` gives the two side
   columns exactly equal width by definition, so the brand is centred in the bar no
   matter what is in them — the back button that comes and goes, a step count that is
   there on five screens out of ten, or neither. A fixed side width would have to be
   guessed against the widest step count and would be wrong the first time one grew. */
.bar {
  flex: none; position: relative; z-index: 4;
  display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: center; gap: 9px;
  padding: 6px var(--gut) 10px;
}
.bar-brand { display: inline-flex; align-items: center; gap: 8px; }
/* ⚠ 44, AND THE GLYPH STAYS WHERE IT WAS. At 34 this was the one control in the app
   under the floor — on every screen but the first, on every device — while looking
   perfectly normal, because a chevron is small and the box around it is invisible.
   The negative margin keeps the icon optically on the gutter while the target grows
   outward into the padding. */
.bar-back {
  justify-self: start;
  width: 44px; height: 44px; margin-left: -13px; flex: none;
  display: grid; place-items: center;
  border-radius: 50%; color: var(--ink);
  opacity: 0; pointer-events: none;
  transition: opacity 200ms ease, background 160ms ease;
}
.bar-back svg { width: 21px; height: 21px; display: block; }
.bar-back:hover { background: var(--hair-2); }
/* ⚠ THE FIRST SCREEN HAS NOWHERE TO GO BACK TO, and a chevron that answers nothing
   is worse than no chevron: it reads as the flow having lost its history. */
body[data-canback='1'] .bar-back { opacity: 1; pointer-events: auto; }
.bar-mark { width: 19px; height: 19px; flex: none; display: block; }
.bar-mark svg { width: 100%; height: 100%; display: block; fill: var(--ink); }
.bar-t { font-size: 14px; font-weight: 600; letter-spacing: -0.014em; }
.bar-step {
  justify-self: end;
  font-size: 11.5px; font-weight: 600; letter-spacing: 0.02em;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* ══ THE SCREENS ════════════════════════════════════════════════════════════════ */
.screens { position: relative; flex: 1 1 auto; min-height: 0; }
.scrn {
  position: absolute; inset: 0;
  display: none; flex-direction: column;
  padding: 4px var(--gut) max(38px, calc(env(safe-area-inset-bottom) + 20px));
  overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
.scrn::-webkit-scrollbar { width: 0; }
.scrn.on { display: flex; animation: enter 340ms cubic-bezier(0.2, 0.7, 0.25, 1); }
/* ⚠ BACKWARDS TOO. Played forwards on the way back the flow reads as still going
   forwards, which is the one thing a back button has to contradict. */
body[data-dir='back'] .scrn.on { animation-name: enterBack; }
@keyframes enter     { from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: none; } }
@keyframes enterBack { from { opacity: 0; transform: translateX(-16px); } to { opacity: 1; transform: none; } }
.spacer { flex: 1 1 auto; min-height: 14px; }

/* ══ TYPE ═══════════════════════════════════════════════════════════════════════ */
.eyebrow {
  margin: 0 0 12px;
  font-size: 11px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--muted);
}
/* ⚠ THE HEADLINE'S SIZE IS A CLAMP ON THE VIEWPORT, NOT A FIXED PT. At 320 the
   prototype's 27px ran "prioritizing" onto its own line and left a widow; at 430 it
   was small in a lot of paper. Every screen in the flow uses the same two sizes so
   the eye never has to re-find the headline. */
.head {
  margin: 0;
  font-size: clamp(25px, 7.1cqw, 30px);
  font-weight: 700; line-height: 1.1; letter-spacing: -0.03em;
  text-wrap: balance;
}
.q {
  margin: 0 0 9px;
  font-size: clamp(21px, 5.9cqw, 25px);
  font-weight: 700; line-height: 1.14; letter-spacing: -0.028em;
  text-wrap: balance;
}
/* the note belongs to the question, so it stays close to it — the air goes between
   the pair and the answers */
.sub {
  margin: 8px 0 0; max-width: 32ch;
  font-size: 14.5px; line-height: 1.46; color: var(--muted); letter-spacing: -0.004em;
}
.note {
  margin: 0 0 26px;
  font-size: 11px; font-weight: 600; letter-spacing: 0.085em; text-transform: uppercase;
  color: var(--muted); opacity: 0.8;
}

/* ⚠ THE STRIKE IS THE ACCENT AND IT IS DRAWN, NOT `text-decoration`. A line-through
   is a hairline that sits on the baseline grid and cannot be timed; this is a bar
   that grows from the left after the screen has landed, which is the gesture the
   sentence is making. The struck words go grey so the live half is the black one. */
.head s { text-decoration: none; color: var(--muted); position: relative; display: inline-block; }
.head s::after {
  content: ""; position: absolute; left: -3px; right: -3px; top: 52%; height: 3px;
  background: var(--accent); border-radius: 2px;
  transform: rotate(-1.4deg) scaleX(0); transform-origin: left center;
}
.scrn.on .head s::after { animation: strike 420ms cubic-bezier(0.3, 0.75, 0.3, 1) 380ms forwards; }
@keyframes strike { to { transform: rotate(-1.4deg) scaleX(1); } }

/* ══ THE PROGRESS ═══════════════════════════════════════════════════════════════ */
/* ⚠ THE ROOM ABOVE AND BELOW A QUESTION IS THE WHOLE SCREEN'S COMPOSITION. At
   16/4/14 the progress bar, the question, its note and the first chip were four
   things stacked with no rhythm between them — the headline had nothing around it
   and read as a caption on the dots rather than as the thing being asked. */
.dots { display: flex; gap: 5px; margin: 10px 0 30px; }
.dots i { height: 3px; flex: 1; border-radius: 2px; background: var(--hair); }
.dots i.done { background: var(--ink); }
.dots i.now  { background: var(--accent); }

/* ══ THE BUTTONS ════════════════════════════════════════════════════════════════
   ⚠ 50, NOT 44. This is the one action on the screen and it is pressed with a thumb
   at the bottom of a phone; the landing page's own hero button is 48 and this is the
   same button one screen further in. */
.cta {
  width: 100%; margin-top: 14px; flex: none;
  min-height: 50px; padding: 0 20px;
  border-radius: 999px;
  background: var(--ink); color: var(--paper);
  font-size: 16px; font-weight: 600; letter-spacing: -0.008em;
  transition: transform 180ms cubic-bezier(0.22, 1, 0.36, 1), opacity 180ms ease;
}
.cta:hover  { transform: translateY(-1.5px); }
.cta:active { transform: translateY(0); opacity: 0.86; }
/* ⚠ DISABLED IS DIM, NOT INVISIBLE. At the prototype's 0.26 the button read as
   absent on a white ground and the screen looked like it had no way forward. */
.cta[disabled] { opacity: 0.34; cursor: not-allowed; transform: none; }
.cta.accent { background: var(--accent); color: #fff; }
.ghost {
  width: 100%; min-height: 44px; margin-top: 2px; flex: none;
  font-size: 14.5px; font-weight: 500; color: var(--muted);
  transition: color 160ms ease;
}
.ghost:hover { color: var(--ink); }

/* ══ THE CHOICES ════════════════════════════════════════════════════════════════ */
.choices { display: flex; flex-direction: column; gap: 8px; }
/* ⚠ THE ANSWERS SIT LOW, NOT UNDER THE QUESTION — Joonas, 2026-09-06: "for multiple
   choice, we can put answers a little bit lower so that the user taps it easier."
   A phone is held at the bottom and the top third of a tall screen is the furthest
   thing from a thumb, so the free space goes ABOVE the choices rather than below
   them: the question stays at the top where it is read, and the block you have to
   touch is pushed down to meet your hand.
   ⚠ SCOPED TO THE QUESTION SCREENS. `.spacer` is used on the cards and the waitlist
   too, and those are compositions rather than reachability problems. */
.scrn[data-id^='q'] .choices { margin-top: auto; }
.scrn[data-id^='q'] .otherfield.on { margin-top: 8px; }
.scrn[data-id^='q'] .spacer { flex: 0 0 16px; }
/* ⚠ AND THE THREE CARDS SIT LOW FOR THE SAME REASON, WHICH ONLY BECAME TRUE ON
   2026-09-08. The note above says this screen is a composition rather than a
   reachability problem, and while there was a ghost button under the cards that was
   right — the block ended near the foot of the screen and the spacer was holding the
   two apart. With that button gone the spacer had nothing below it, so all of the
   free height pooled UNDER the three things you are being asked to touch: the cards
   sat in the top half of a 874-tall phone with five hundred pixels of nothing under
   them, which is both further from a thumb and the exact pooling this file complains
   about in three other places. */
.scrn[data-id='cards'] .picks { margin-top: auto; }
.scrn[data-id='cards'] .spacer { flex: 0 0 6px; }
.chip {
  display: flex; align-items: center; gap: 11px; width: 100%; text-align: left;
  border: 1.5px solid var(--hair); border-radius: var(--r);
  background: var(--bg);
  /* ⚠ A REAL 52. Six of these fill a phone and every one is a tap target; the
     prototype's 13px padding gave 47 at the shortest label. */
  min-height: 52px; padding: 12px 14px;
  font-size: 15px; line-height: 1.25; letter-spacing: -0.012em; font-weight: 500;
  transition: border-color 130ms ease, background 130ms ease, transform 130ms ease;
}
.chip:hover  { border-color: rgba(17, 19, 16, 0.28); }
.chip:active { transform: scale(0.994); }
.chip .box {
  width: 20px; height: 20px; flex: none;
  border-radius: 6px; border: 1.5px solid var(--hair);
  display: grid; place-items: center;
  transition: background 130ms ease, border-color 130ms ease;
}
.chip.single .box { border-radius: 50%; }
.chip .box svg { width: 11px; height: 11px; stroke: #fff; stroke-width: 2.6; fill: none; opacity: 0; transition: opacity 130ms ease; }
.chip[aria-pressed='true'] { border-color: var(--ink); }
.chip[aria-pressed='true'] .box { background: var(--ink); border-color: var(--ink); }
.chip[aria-pressed='true'] .box svg { opacity: 1; }
.chip.other { color: var(--muted); }
.chip.other .arw { margin-left: auto; color: var(--muted); opacity: 0.6; }
.otherfield {
  display: none; margin-top: 8px;
  border: 1.5px solid var(--ink); border-radius: var(--r);
  background: var(--bg); padding: 13px 14px;
}
.otherfield.on { display: block; }
/* ⚠ 16px, AND IT IS NOT A TYPE DECISION. iOS zooms the whole page on focus for any
   input under 16, and the flow never zooms back out. Every field here is 16. */
.otherfield input { width: 100%; border: 0; background: none; outline: none; font-size: 16px; font-weight: 500; }
.otherfield input::placeholder { color: var(--muted); opacity: 0.65; font-weight: 400; }

/* ══ THE THREE CARDS ═══════════════════════════════════════════════════════════
   ⚠ THREE DESIGNS, ON A SWITCH, AND THE SWITCH IS A REVIEW CONTROL. This is the one
   screen in the flow that has to sell something, so it is worth trying more than one
   answer to — the chooser at the foot of it is temporary and comes out the moment one
   is picked. Everything that varies is in the three blocks at the bottom of this
   section; everything above them is shared, so a variant can only change how the
   cards LOOK and never what they say.

   ⚠ WHAT THEY ALL FIX: the first pass was two near-black slabs and one saturated blue
   one, stacked. Three heavy blocks on white is a wall — the eye reads the WEIGHT
   before it reads any of the three things being offered, which on a screen whose
   whole job is "pick one" is the wrong order. All three of these are light. */
.picks { display: flex; flex-direction: column; gap: 12px; }

.pick {
  display: flex; gap: 14px; align-items: flex-start; width: 100%; text-align: left;
  border-radius: 20px; padding: 16px;
  background: var(--bg); color: var(--ink);
  transition: transform 180ms cubic-bezier(0.22, 1, 0.36, 1), box-shadow 220ms ease;
}
.pick:active { transform: scale(0.995); }
.pick .ptxt { flex: 1; min-width: 0; }
/* ⚠ EVERY ONE OF THESE IS SCOPED TO `.ptxt`, AND THAT IS NOT TIDINESS. The card now
   carries a LIVE WIDGET out of the bank, which is a foreign fragment with its own
   markup in it — and `.pick b` is a descendant selector, so the moment a widget
   containing a `<b>` was mounted here it inherited this card's 15.5px display block.
   The `today` tile did: its heading came out at 15.5px in a tile scaled to 0.43, i.e.
   two and a half times the size the widget draws it, running off its own edge and
   clipped by the tile. It looks exactly like a widget that does not scale, which is
   where a round of work went before the font-size was measured on both sides.
   ⚠ `.min` AND `.src` ARE SCOPED FOR THE SAME REASON even though nothing in the bank
   uses those class names today. The bank is vendored and grows; the rule that keeps
   this working is that the card's typography stops at the card's own text. */
.pick .ptxt b { display: block; font-size: 15.5px; font-weight: 600; letter-spacing: -0.016em; line-height: 1.24; }
.pick .ptxt .pd { display: block; margin-top: 3px; font-size: 13px; line-height: 1.38; color: var(--muted); }
.pick .ptxt .pmeta { display: flex; align-items: center; gap: 8px; margin-top: 11px; flex-wrap: wrap; }
.pick .ptxt .min {
  flex: none; border-radius: 999px; padding: 3.5px 9px;
  background: var(--fill); color: var(--muted);
  font-size: 11px; font-weight: 600; letter-spacing: 0.01em;
}
.pick .ptxt .src { font-size: 11.5px; letter-spacing: 0.004em; color: var(--muted); opacity: 0.72; }

/* ⚠ THE LEAD SAYS WHY IT IS FIRST IN WORDS. Ranked 1/2/3 in a numbered disc, the
   difference between the recommendation and the alternatives is a digit somebody has
   to notice and interpret; two words say it outright, and then the number is free to
   go and take a whole column of chrome with it. */
.pick .ptxt .lead-tag {
  display: none;
  margin-bottom: 6px;
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--accent);
}
.pick.lead .ptxt .lead-tag { display: block; }

/* ── the app each card would build ──────────────────────────────────────────
   ⚠ THERE WAS A DRAWN SODA BOTTLE HERE AND IT IS GONE. Every card carried one, in a
   40px disc, and the three of them were the same emblem three times — which is a
   decoration saying "task", on a screen where each card is a DIFFERENT thing being
   offered. The live widget says which one.

   ⚠ 60, NOT 44. At 44 the bank's tiles are the size the flow uses for a chat card's
   thumbnail and everything inside one is a texture rather than a picture — a 140px
   design scaled to 0.31, so the calendar's day letters are two pixels tall. At 60 the
   same tile is scaled 0.43 and reads as the app it is; and it is the size the card
   can carry, because the card's own type sets its height and three lines of it is
   about 68px. Above that the widget starts leading the card and the sentence saying
   what the task IS becomes the second thing read.

   ⚠ AND THE RADIUS IS A SHARE OF THE TILE, NOT A CONSTANT. 15 on 60 is the same
   corner the bank's own home screen gives a widget at its design size, so the preview
   here and the bigger one on the last screen are recognisably the same object. */
.pick .vw {
  /* ⚠ SIZE AND CORNER ONLY. `.vw` already carries the clip and the hairline ring every
     tile on this page gets — a third of the bank is a near-white artwork with no edge
     of its own — and restating them here is a second place for them to drift. */
  width: 60px; height: 60px; border-radius: 15px;
}

/* ── A · paper — white cards, floating ─────────────────────────────────────── */
.scrn[data-style='paper'] .pick {
  box-shadow: 0 1px 2px rgba(17, 19, 16, 0.05),
              0 10px 24px -14px rgba(17, 19, 16, 0.30);
}
.scrn[data-style='paper'] .pick:hover { transform: translateY(-2px); box-shadow: 0 2px 4px rgba(17,19,16,.06), 0 18px 34px -16px rgba(17,19,16,.34); }
/* ⚠ THE LEAD IS A GLOW, NOT A FILL. Filled, it is the loudest object on a screen of
   three equals and the other two read as rejected; glowing, it is the same card with
   a light on it. The ring is what keeps it legible when the shadow is clipped. */
.scrn[data-style='paper'] .pick.lead {
  box-shadow: inset 0 0 0 1.5px rgba(10, 101, 245, 0.20),
              0 2px 5px rgba(10, 101, 245, 0.10),
              0 16px 38px -16px rgba(10, 101, 245, 0.55);
}
.scrn[data-style='paper'] .pick.lead:hover { transform: translateY(-2px); }

/* ⚠ THE PAPER STYLE USED TO HIDE THE WIDGET AND SHOW THE EMBLEM, and that line is
   what this screen's whole change is. There was a second card style called `apps`
   whose one idea was that a card carries the app it would build; it was cut on
   2026-09-06 as one variant too many, and asked for again on 2026-09-08 — as the
   only style there is, rather than as a choice. */

/* ══ THE THREAD ═════════════════════════════════════════════════════════════════
   sota's OWN bubble shape, lifted from the landing phone's chat: ink on the right,
   the card grey on the left, one corner tightened on the side it is spoken from.
   The prototype used an accent-tinted panel with a left rule; this brand already
   has a chat and inventing a second one would be two answers to one question. */
/* ⚠ THERE IS NO `.chat-head`. It was the mark plus `sabi` / `main thread` over a
   hairline at the top of this screen, removed 2026-09-07: a thread inside a product
   does not introduce the product to the person already in it, and the host owns the
   top of the screen anyway. Its 57px went to the thread. */

.thread { display: flex; flex-direction: column; gap: 8px; }
.bub {
  max-width: 84%; border-radius: 16px; padding: 11px 13px;
  font-size: 14.5px; line-height: 1.4; letter-spacing: -0.008em;
}
/* ⚠ THE ASSISTANT'S TURNS ARE NOT IN A BUBBLE — Joonas, 2026-09-06: "when we go into
   the chat, can we make it look more like our chat?" Two grey-and-black bubbles
   trading places is iMessage, and it makes the assistant one of two equal
   correspondents. Every assistant chat worth copying does the opposite: what YOU said
   is a contained object, and what it said is just text on the page, set a shade larger
   because it is the thing being read. The only bubble left is yours. */
.bub.bot {
  align-self: stretch; max-width: 100%;
  background: none; padding: 2px 0 4px;
  font-size: 15.5px; line-height: 1.45;
}
.bub.you {
  align-self: flex-end; background: var(--fill); color: var(--ink);
  border-bottom-right-radius: 5px;
}

/* ⚠ THE DOTS ARE A BUBBLE, not a row of dots floating on the ground. Anything else
   reads as the app thinking about the page rather than as a turn being composed. */
.typing {
  align-self: flex-start; display: flex; gap: 4px; align-items: center;
  background: var(--fill); border-radius: 16px; border-bottom-left-radius: 5px;
  padding: 14px 14px;
}
.typing i { width: 5px; height: 5px; border-radius: 50%; background: var(--muted); opacity: 0.4; animation: blink 1.1s infinite; }
.typing i:nth-child(2) { animation-delay: 0.18s; }
.typing i:nth-child(3) { animation-delay: 0.36s; }
@keyframes blink { 0%, 60%, 100% { opacity: 0.28; } 30% { opacity: 0.9; } }

/* ⚠ THE QUICK REPLIES ARE RIGHT-ALIGNED, because they are things YOU are about to
   say and every other thing you have said is on that side. Left-aligned they read as
   more of the assistant's turn. */
/* ⚠ THE CHOICES ARE THE SAME OBJECT AS THE ANSWERS ON THE QUESTION SCREENS — Joonas,
   2026-09-06: "the choices should be a little bit more visible and can basically be
   more like previous variant choices." They were small right-aligned pills wrapping in
   the corner, which reads as a hint about what you COULD type rather than as the thing
   to press. Full width, left-aligned, a real border and a 46px target: a reader who
   has just answered five screens of exactly this shape knows what it is on sight. */
.qr { display: flex; flex-direction: column; gap: 7px; margin-top: 14px; flex: none; }
.qr button {
  /* the answers' own shape: 1.5px, the shared radius, left-aligned, a 46px target */
  width: 100%; text-align: left;
  border: 1.5px solid var(--hair); border-radius: var(--r);
  background: var(--bg); color: var(--ink);
  min-height: 46px; padding: 12px 14px;
  font-size: 15px; font-weight: 500; line-height: 1.25; letter-spacing: -0.012em;
  opacity: 0; transform: translateY(4px); animation: pop 300ms ease forwards;
  transition: border-color 140ms ease, background 140ms ease;
}
.qr button:nth-child(2) { animation-delay: 0.05s; }
.qr button:nth-child(3) { animation-delay: 0.10s; }
.qr button:nth-child(4) { animation-delay: 0.15s; }
.qr button:hover { border-color: rgba(17, 19, 16, 0.3); background: var(--fill); }
@keyframes pop { to { opacity: 1; transform: none; } }

.composer {
  display: flex; align-items: center; gap: 9px; flex: none;
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--hair-2);
}
.composer input {
  flex: 1; min-width: 0;
  border: 1px solid var(--hair); border-radius: 999px; background: var(--bg);
  min-height: 44px; padding: 0 15px; font-size: 16px; outline: none;
}
.composer input::placeholder { color: var(--muted); opacity: 0.7; }
.composer button {
  width: 44px; height: 44px; flex: none; border-radius: 50%;
  background: var(--ink); color: var(--paper);
  display: grid; place-items: center;
  transition: opacity 160ms ease, transform 160ms ease;
}
.composer button svg { width: 18px; height: 18px; display: block; }
.composer button[disabled] { opacity: 0.22; }
.composer button:not([disabled]):active { transform: scale(0.94); }

/* ══ THE WAITLIST ═══════════════════════════════════════════════════════════════ */
.emailfield {
  display: flex; align-items: center; gap: 8px; flex: none; margin-top: 14px;
  border: 1.5px solid var(--hair); border-radius: 999px;
  background: var(--bg); padding: 5px 5px 5px 16px;
  transition: border-color 160ms ease;
}
.emailfield:focus-within { border-color: var(--ink); }
/* ⚠ THE PILL BEING TALL IS NOT THE FIELD BEING TALL. The input measured 22px inside
   a 54px pill, so the only part of it a tap can focus was a third of what it looks
   like — and on the one screen in the flow that asks for typing. */
.emailfield input { flex: 1; min-width: 0; min-height: 44px; border: 0; background: none; outline: none; font-size: 16px; }
.emailfield input::placeholder { color: var(--muted); opacity: 0.7; }
.emailfield button {
  flex: none; min-height: 44px; padding: 0 18px; border-radius: 999px;
  background: var(--ink); color: var(--paper);
  font-size: 14.5px; font-weight: 600; letter-spacing: -0.006em;
  transition: opacity 160ms ease;
}
.emailfield button[disabled] { opacity: 0.24; }
.done {
  display: none; flex: none; margin-top: 14px;
  border-radius: var(--r); background: var(--fill);
  padding: 14px; font-size: 13.5px; line-height: 1.45;
}
.done.on { display: block; animation: pop 350ms ease; }
.done b { color: var(--ok); font-weight: 600; }

/* ══ THE VISUALS ════════════════════════════════════════════════════════════════
   ⚠ NONE OF THEM IS A PICTURE OF THE PRODUCT. Every widget below is a LIVE one out
   of the same bank the landing page's phone draws from — built once, drawn on a
   shared clock — so the thing the copy promises is the thing on the screen. A
   screenshot would have been quicker and would have been a claim rather than a
   demonstration.

   ⚠ AND THE FIRST TWO CARRY NO FRAME. They depict the app's own chat, so a border
   round them turns the app into an illustration of itself. Only the third is a box,
   because the third is an object behind glass. */

/* the host a widget is mounted into.
   ⚠ THE CORNER BELONGS TO THE HOST AND THE FIELD HAS NONE, and getting that backwards
   made the app icon A DIFFERENT SHAPE IN EVERY BROWSER WINDOW. `container-type` sets
   up a container for an element's DESCENDANTS, never for the element itself — so a
   `border-radius: 8.5cqw` written on the field resolves against the nearest ANCESTOR
   container, and there is none on this page, so it falls back to the VIEWPORT.
   Measured: 33.15px on a 390 phone and 122.4px on a 1440 desktop, on a tile 52px
   across. The host clips at 12px and the circle is already inside that clip, so the
   host's own radius could never correct it — every widget rendered as a disc on a
   desk and a squircle on a phone, with nothing in either rule saying why.
   ⚠ AND IT MUST BE A PX ON THE HOST, not on the field: the field is laid out at 140
   and SCALED, so a radius written there is divided by the same factor. */
/* ⚠ EVERY TILE TAKES A RING, and it is not decoration. A third of the bank is a
   near-white artwork — a calendar grid, a checklist, a note — and mounted on the
   white card that announces it, those have no edge at all and read as a hole rather
   than as an app. The ring belongs to "a widget shown as an icon" and not to any one
   widget, so it lives here. It is the widget playground's own elevation. */
.vw {
  position: relative; display: block; flex: none;
  overflow: hidden; border-radius: 12px;
  box-shadow: 0 0 0 1px rgba(17, 19, 16, 0.07),
              0 1px 2px rgba(17, 19, 16, 0.05),
              0 6px 14px -10px rgba(17, 19, 16, 0.35);
}
.vw > .field {
  container-type: inline-size;
  position: absolute; inset: 0 auto auto 0;
  display: block; overflow: hidden;
  transform-origin: 0 0;
  border-radius: 0;
  background: #FAFAFA;
  isolation: isolate;
}

/* ── visual one: the loop ────────────────────────────────────────────────── */
/* ⚠ BOTTOM-ALIGNED AND MASKED AT THE TOP. The loop runs four beats through a fixed
   box, so something has to leave — and a turn sliced by a hard edge reads as a
   layout that ran out of room where one that dissolves reads as a thread with more
   above it. It is a mask and not a painted scrim, because the ground behind it is
   white here and a widget card there. */
/* ⚠ IT TAKES THE FREE SPACE RATHER THAN BEING GIVEN A HEIGHT. Sized at a clamp it
   came out 337px on an 844 phone with the headline jammed against its last card and
   FOUR HUNDRED PIXELS of nothing between the lead and the button — every bit of air
   on the screen pooled in the one place it did nothing. Growing, the thread bottoms
   out a fixed gap above the headline at every height, and the block of type sits on
   the lower third where a thumb is. */
/* ── visual one: the person's own phone ───────────────────────────────────────
   ⚠ THE LANDING PAGE'S OWN HERO, ON THE SCREEN THE HERO SENDS PEOPLE TO — Joonas,
   2026-09-08. Not a picture of it: the same parts `js/app.js` builds, out of the same
   `PERSONAS[key].screen`, so the two cannot drift and there is no bitmap to re-capture
   when a widget or a colour moves.

   ⚠ AND IT IS INSIDE THE PHONE — Joonas, 2026-09-09: "the different screens should be
   within the phone frame. It looks weird if they're not." The first build put the rail
   and rows straight onto the onboarding's own paper, on the reasoning that a phone
   inside a phone is a photograph of a desk. That reasoning is right about a DEVICE
   somebody is meant to read as the thing in their hand, and wrong here: this is a
   picture the hero already established, and without the bezel eight tiles read as
   widgets loose on a page rather than as somebody's home screen. The hero's image IS a
   phone, so this is a phone.

   ⚠ EVERY NUMBER BELOW IS `css/app.css`'s, and they are copied rather than imported
   because the onboarding does not load the landing page's stylesheet and loading it
   would restyle this whole page. The two that matter are the device aspect (0.48655)
   and the screen's inset, which is deliberately LOOSER than the glass with a TIGHTER
   radius — measured exactly onto the hole the two roundings never agree to the pixel
   and the mismatch shows as pale wedges in the corners. */
.scrn[data-id='s1'] .vhost { container-type: inline-size; }
.vwall {
  /* ⚠ IT GROWS INTO THE BAND AND IS CUT BY THE MASK. A fixed height is a guess about
     the phone the page is being read on; taking the room that is there and dissolving
     at the bottom of it is the same picture on every screen, which is what "fits the
     space" means when the space is different on every device. */
  height: 100%;
  overflow: hidden;
  display: flex; justify-content: center;
  /* ⚠ THE MASK IS ON THE WALL, NOT ON `.vhost`. The host is the plain clipping box the
     other two visuals also use, and a fade there would be a fade under every screen's
     visual — including the tracker on screen two, whose bottom row of bars is the
     thing that screen is about.
     ⚠ AND IT TAKES THE DEVICE WITH IT. The bezel fading out is the point: a phone with
     a hard horizontal cut across it is a screenshot somebody trimmed, and a phone
     dissolving is a picture carrying on past the frame. */
  -webkit-mask-image: linear-gradient(180deg, #000 58%, rgba(0, 0, 0, 0.34) 84%, transparent 100%);
          mask-image: linear-gradient(180deg, #000 58%, rgba(0, 0, 0, 0.34) 84%, transparent 100%);
}
/* ⚠ THE ASPECT IS THE DEVICE'S AND THE HEIGHT FOLLOWS FROM IT, so the phone is always
   taller than the band and is always cut — which is the composition the reference has.
   `width` is capped so it never outgrows the column on a wide desk frame. */
.vphone {
  position: relative;
  flex: none; align-self: flex-start;
  width: min(80%, 272px);
  aspect-ratio: 0.48655;
}
/* see the note above for why this is looser than the glass */
.vscreen {
  position: absolute; z-index: 1;
  inset: 0.9% 3.3% 1.0% 3.25%;
  border-radius: 13.5% / 6.4%;
  background: #FFFFFF;
  overflow: hidden;
  display: flex; flex-direction: column;
  /* ⚠ THE CONTAINER IS THE SCREEN, NOT THE PHONE. Every length inside is a share of
     the glass you can actually see — measured on the phone it would include the bezel
     and the whole design would come out ~7% small in a box nothing can be put in. */
  container-type: inline-size;
}
.vframe { position: relative; z-index: 2; display: block; width: 100%; height: 100%; pointer-events: none; }
.vscr-top {
  display: flex; align-items: center; justify-content: space-between;
  padding: 12cqw 5cqw 3cqw;
  flex: none;
}
.vscr-mark { display: block; width: 8.2cqw; height: 8.2cqw; }
.vscr-mark svg { display: block; width: 100%; height: 100%; fill: var(--ink); }
/* ⚠ A SQUIRCLE, NOT A CIRCLE — Joonas, 2026-09-07: "let's have all of these be
   squircle shaped vs round actually throughout." A round photograph is the convention
   for a person, which is exactly why it was the last round thing on the screen: one
   round object among twenty squircles does not read as "this one is a person", it
   reads as one element nobody got to. */
/* ⚠ A ROUNDED SQUARE, AND NOT `clip-path: url(#sqTile)` — the landing page's avatar
   uses the real squircle path, and an SVG `clipPath` reference in CSS is DOCUMENT
   scoped: `#sqTile` is defined in the landing page's own index.html, so on this page
   the url resolves to nothing and the clip silently does nothing at all. It rendered
   as a hard-cornered square, which is the one shape nothing else here is.
   Copying two kilobytes of path into a second document for one 28px image is the
   wrong trade when every other tile on this screen is already a plain radius — so the
   avatar takes the same treatment they do, and the page has one shape language. */
.vscr-me { width: 8.2cqw; height: 8.2cqw; border-radius: 2.6cqw; object-fit: cover; display: block; }
.vscr-body { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; gap: 3.4cqw; }

/* the rail — two rows flowing in COLUMNS and running off the right, and these are the
   hero's own cell size and gutter because this is the hero's own container */
.vwrail {
  flex: none;
  display: grid;
  grid-template-rows: 1fr 1fr;
  grid-auto-flow: column;
  grid-auto-columns: 36cqw;
  gap: 3cqw;
  padding: 1cqw 0 0 5cqw;   /* the left gutter only — the right runs off */
}
.vwcell {
  position: relative;
  width: 36cqw; height: 36cqw;
  border-radius: 6cqw;
  overflow: hidden;
  background: #FAFAFA;
  box-shadow: 0 1cqw 3.5cqw rgba(0, 0, 0, 0.10);
}
/* ⚠ THE FIELD IS LAID OUT AT 140 AND SCALED — see `mount()` in visuals.js. A widget
   laid out at its display size is mud: everything inside one is a cqw of its own tile,
   so at 56pt the labels are two pixels tall.
   ⚠ AND `inset` IS A SHORTHAND. Written `left:0; top:0; inset:auto` the reset comes
   last and undoes both, and every field falls back to its static position — the bug
   `css/app.css` has a note about, and the reason this is one edge list. The field is
   sized in px by `mount` and must not be stretched, so only the top-left is pinned. */
.vwcell > .field,
.vwic > .field {
  container-type: inline-size;
  position: absolute; inset: 0 auto auto 0;
  display: block; overflow: hidden;
  transform-origin: 0 0;
  border-radius: 0;
  background: #FAFAFA;
  isolation: isolate;
}
.vwlist { flex: none; display: flex; flex-direction: column; gap: 2.4cqw; padding: 0 5cqw; }
.vwrow {
  display: flex; align-items: center; justify-content: space-between;
  gap: 3cqw; width: 100%;
  padding: 3cqw;
  border-radius: 5cqw;
  background: rgba(147, 147, 147, 0.08);
}
.vwrow:first-child { box-shadow: inset 0 0 0 1.5px #fff; }
.vwic { flex: none; width: 14.4cqw; height: 14.4cqw; position: relative; border-radius: 4cqw; overflow: hidden; }
.vwav { width: 100%; height: 100%; object-fit: cover; display: block; border-radius: 4cqw; }
.vwdot { display: block; width: 100%; height: 100%; border-radius: 4cqw; }
.vwtxt { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0.3cqw; }
.vwtxt b {
  font-size: 4.4cqw; line-height: 5.2cqw; font-weight: 600; letter-spacing: -0.014em;
  color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.vwtxt span {
  font-size: 4cqw; line-height: 4.8cqw; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.vwwhen {
  flex: none; display: flex; align-items: center; gap: 2cqw;
  font-size: 3.8cqw; line-height: 4.4cqw; color: var(--muted); opacity: 0.7;
}
.vwwhen i { display: block; width: 1.9cqw; height: 1.9cqw; border-radius: 999px; background: var(--accent); }

.vloop {
  position: relative;
  flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column; justify-content: flex-end;
  /* ⚠ THIS BOX HAS TO CLIP — the fades are a mask and a mask needs an edge — so the
     room for what hangs OUTSIDE a card is bought with padding and given back with a
     negative margin. Without it the widget cards' own drop shadows were sliced flat
     against the left and right of the thread: `overflow: hidden` clips at the
     PADDING box, and with no padding that is exactly where a card's border box ends.
     A shadow cut off by a straight vertical line does not read as a clipping bug, it
     reads as a card that has no shadow on one side. */
  padding-inline: 18px;
  margin-inline: -18px;
  overflow: hidden;
  /* ⚠ IT DISSOLVES AT BOTH ENDS NOW. Fading only the top left the thread ending on a
     hard edge with a dark bubble sitting against the headline — the visual read as a
     BLOCK that stops rather than as something the page continues past. The bottom
     fade is shorter than the top one and the thread is padded to meet it, so the
     newest turn keeps its type and only its lower edge goes into the paper. */
  /* ⚠ THE BOTTOM FADE IS SHORTER THAN THE THREAD'S OWN PADDING, so the turn you are
     reading is clear of it and only a turn still SLIDING UP passes through it. A
     fade deep enough to see at rest is a fade eating the newest thing said. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 56px, #000 calc(100% - 30px), transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 56px, #000 calc(100% - 30px), transparent 100%);
}
.vloop .thread { gap: 7px; padding-bottom: 42px; }
/* ⚠ A TURN OF YOURS GETS TEN MORE PIXELS ABOVE IT — Joonas, 2026-09-14. At the
   thread's flat 7px gap a card the assistant had just handed back and the next
   thing you say read as one run of turns arriving together; they are the two sides
   of the conversation, and the gap is what says a new exchange is starting. On top
   of the gap rather than instead of it, so bot-to-bot spacing is untouched.
   ⚠ `:not(:first-child)`, because the first thing in the thread has nothing above
   it to be spaced from and the margin would push the whole thread down into the
   mask's top fade. */
.vloop .thread .bub.you:not(:first-child),
/* ⚠ THE PHOTO IS A TURN OF YOURS TOO. `.vshot` is `align-self: flex-end` on the
   ink ground — it is the same side of the conversation as `.bub.you` and two of
   the four beats open with one, so leaving it out spaces half the exchanges. */
.vloop .thread .vshot:not(:first-child) { margin-top: 10px; }
.vloop .bub { font-size: 13.5px; padding: 9px 12px; border-radius: 14px; }
/* every turn arrives rather than appearing */
/* ⚠ SLOWER, AND WITH A CURVE THAT HAS A SOFT HEAD AS WELL AS A SOFT TAIL.
   `cubic-bezier(0.22,1,0.36,1)` spends most of its travel in the first fifth, so a
   turn at 380ms LEAPT into place and settled — which reads as a UI updating rather
   than as somebody speaking. Over 540ms on a gentler curve, with a little more
   travel and a breath of blur coming off, it reads as arriving. */
/* ⚠ NO `filter` IN AN ENTRANCE. A blur is re-rasterised every frame, and these turns
   arrive while two live widgets are drawing on the same clock — which is what made
   them janky rather than soft. Opacity and transform are the only two properties a
   compositor can animate without touching the main thread, so that is all these do.
   ⚠ AND THE TRAVEL MOVED OUT OF THE TURN AND INTO THE THREAD. The thread is
   bottom-aligned, so appending a turn shoves everything above it upward in ONE
   frame — a jump nothing was animating, under a bubble that was politely sliding in.
   `push()` in visuals.js now slides the whole thread by exactly what the new turn
   added, and the turn itself only fades. That is how a real chat feels. */
.vin { animation: vin 460ms cubic-bezier(0.33, 0.9, 0.35, 1) both; }
@keyframes vin {
  from { opacity: 0; transform: scale(0.99); }
  to   { opacity: 1; transform: none; }
}

/* the photo the schedule arrives as. ⚠ IT IS DRAWN, not a bitmap — a real
   photograph of a real timetable is a licence question on a page that has none. */
.vshot {
  align-self: flex-end; display: flex; align-items: center; gap: 9px;
  background: var(--ink); color: var(--paper);
  border-radius: 14px; border-bottom-right-radius: 5px; padding: 8px 12px 8px 8px;
  max-width: 84%;
}
.vshot .px { width: 40px; height: 40px; flex: none; border-radius: 8px; overflow: hidden; display: block; }
.vshot .px svg { width: 100%; height: 100%; display: block; }
.vshot .pt { font-size: 13px; line-height: 1.3; letter-spacing: -0.008em; }
.vshot .pt em { display: block; font-style: normal; font-size: 11px; color: rgba(245, 244, 241, 0.6); }

/* ⚠ THE ANSWER IS A CARD WITH A RUNNING APP IN IT, and the app is the point of the
   whole screen — "a task turning into a solution" is only a claim until the solution
   is on the page, working. */
.vcard {
  align-self: flex-start; display: flex; align-items: center; gap: 11px;
  background: var(--bg); border: 1px solid var(--hair-2);
  border-radius: 16px; border-bottom-left-radius: 5px; padding: 9px 13px 9px 9px;
  max-width: 92%;
  box-shadow: 0 1px 2px rgba(17, 19, 16, 0.04), 0 8px 18px -12px rgba(17, 19, 16, 0.3);
}
/* ⚠ 72, AND THE NUMBER WAS FOUND BY LOOKING. At 58 a third of the bank is mud on a
   white card — the calendar is a faint grey grid, the checklist is a smudge — and at
   58 they had already been raised from 52 for the same reason. Rendered side by side
   at 58 / 72 / 88 (the sheet is in shots/onb), 72 is where every one of them reads
   as an app icon rather than as a texture. It is also the hero of this screen: the
   whole beat exists to say a working thing arrived, and a thumbnail says it quietly. */
.vcard .vw { width: 72px; height: 72px; border-radius: 16px; }
.vcard .ct { min-width: 0; }
.vcard .ct b { display: block; font-size: 13.5px; font-weight: 600; letter-spacing: -0.014em; }
.vcard .ct span { display: block; margin-top: 2px; font-size: 11.5px; color: var(--muted); }
.vcard .tick {
  width: 19px; height: 19px; flex: none; margin-left: auto;
  border-radius: 50%; background: var(--ok);
  display: grid; place-items: center;
}
.vcard .tick svg { width: 10px; height: 10px; stroke: #fff; stroke-width: 2.8; fill: none; display: block; }
/* the card lands a beat after its bubble and grows out of nothing, which is the
   whole gesture the screen is describing */
/* ⚠ AND THE CARD IS SLOWER STILL, because it is the moment the whole screen is
   about — a thing that was said becoming a thing that runs. It also scales from
   0.94 rather than 0.9: the widget inside it is measured on the frame it appears,
   and while `offsetWidth` no longer cares about the transform, a gentler arrival is
   less of a jump under a reader's eye either way. */
/* the card still grows, because that beat is the moment the screen is about — but on
   transform and opacity only, for the reason above. It carries a LIVE widget, so a
   blur here was re-rasterising a running app sixty times a second. */
.vgrow { animation: vgrow 620ms cubic-bezier(0.33, 0.9, 0.35, 1) both; }
@keyframes vgrow {
  from { opacity: 0; transform: scale(0.94); }
  to   { opacity: 1; transform: none; }
}

/* ── visual two: the same app, re-skinned ────────────────────────────────── */
/* ⚠ IT GROWS AND BOTTOM-ALIGNS, exactly as the loop on screen one does, and for
   both of that screen's reasons. Given a fixed height it left two hundred pixels of
   nothing between the demonstration and the button; and bottom-aligned, the four
   turns arrive by pushing the earlier ones UP rather than by making the box taller,
   so nothing under them moves while somebody is reading. */
.vfluff {
  flex: 1 1 auto; min-height: 0;
  /* ⚠ 18, NOT 8 — Joonas, 2026-09-08: "make sure there's some padding in between the
     message and the widget". At the thread's own 8px the ask and the tracker read as
     two turns of one conversation arriving together; the screen is arguing that you
     SAID a thing and a piece of software CAME BACK, and those are two objects. The
     gap is what says so. */
  display: flex; flex-direction: column; justify-content: flex-end; gap: 18px;
  /* ⚠ NO MASK AND NO CLIP ON THIS ONE, and that follows from `flex: 0` above. A box
     that HUGS its content has real content against both of its edges, so every fade
     put on it lands on something somebody is reading — a top fade washed out the
     first line of the demonstration and a bottom fade washed out the request that is
     the whole point of the screen. The loop is the opposite case: it overflows, so
     its fades are hiding turns that have genuinely left.
     ⚠ AND WITHOUT THE CLIP THE GLOW NEEDS NO INDENT. The fluffy halo is 7px of inset
     under a 9px blur; unclipped it simply bleeds into the screen's own 22px gutter,
     where the earlier `margin-left: 10px` had been pushing the whole card ten pixels
     right of the bubble above it. */
  overflow: visible;
}
.scrn[data-id='s2'] .vhost { overflow: visible; }
.vfluff .bub { font-size: 13.5px; padding: 9px 12px; border-radius: 14px; }

/* the fluffy tracker, as a render.
   ⚠ IT KEEPS THE DRAWN CARD'S WIDTH so the composition on this screen did not move
   when the picture replaced the drawing — 268 is what the bubble above it was measured
   against. Its own aspect does the rest; the fur bleeds soft on every side, so there
   is no radius and no shadow to add: both are already in the pixels. */
.wtimg {
  align-self: flex-start;
  display: block;
  width: min(100%, 268px);
  height: auto;
}

/* ══ THE ANIMATED ONE, WHICH ENDS ON THE SAME PICTURE ══════════════════════════
   ⚠ BOTH LAYERS IN ONE GRID CELL. `grid-area: 1/1` on each child stacks them with
   BOTH still in flow, so the stack is as tall as the taller of the two at every
   moment of the dissolve and nothing under it moves. Absolute positioning would
   take the drawn card out of the flow, leave the stack at the image's height, and
   let the card overlap the ask on every frame where it is the taller — on a box
   (`.vfluff`) whose rule is `overflow: visible` precisely so the fluffy glow can
   bleed past its edge, so there is nothing to clip the mistake either.
   ⚠ AND THE WIDTH IS DECLARED ONCE, on the stack. The two layers agreeing about
   268px is what makes this a dissolve rather than a resize; declared twice it is
   two numbers that can drift apart. */
.wtstack {
  align-self: flex-start;
  display: grid;
  width: min(100%, 268px);
}
.wtstack > * { grid-area: 1 / 1; align-self: start; justify-self: start; width: 100%; }
/* ⚠ THE RENDER IS THE ONE THAT FADES IN, AND THE DRAWN CARD DOES NOT FADE OUT AS
   FAR. It is underneath, so the last of it going is hidden by the picture already
   standing on it — taken to zero on the same curve the two briefly cancel and the
   glow flickers through the middle of the dissolve. */
.wtstack .wtimg { opacity: 0; transition: opacity 520ms ease; }
.wtstack .wt { transition: opacity 420ms ease 120ms; }
.wtstack[data-fur='on'] .wtimg { opacity: 1; }
.wtstack[data-fur='on'] .wt { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .wtstack .wtimg, .wtstack .wt { transition: none; }
}

/* ⚠ ONE OBJECT, TWO SKINS, AND EVERY DIFFERENCE BETWEEN THEM IS A TRANSITIONABLE
   PROPERTY. Nothing is swapped and nothing is rebuilt: the request re-writes the
   tokens on this element and the tracker travels from one design to the other in
   front of you, which is the claim the screen is making. Rebuilding it would show
   the same two pictures and demonstrate nothing.

   ⚠ AND THE WEIGHT ANIMATES, which is only true because the face is a VARIABLE one
   carrying 400-900 in a single file. A two-file 400/700 pair snaps. */
.wt {
  --wt-bg:    #F1F2F0;
  --wt-bg2:   #F1F2F0;
  --wt-ink:   #111310;
  --wt-mute:  #6B6F69;
  --wt-bar:   rgba(17, 19, 16, 0.22);
  --wt-bar2:  rgba(17, 19, 16, 0.22);
  --wt-hi:    #111310;
  --wt-hi2:   #111310;
  --wt-r:     14px;
  --wt-br:    2px;
  --wt-w:     700;
  --wt-glow:  0 0 0 rgba(0, 0, 0, 0);
  --wt-fuzz:  0;
  /* the cute tokens: every one of them is off in the plain skin, so the two designs
     are still ONE object with a different set of numbers on it */
  --wt-gloss: 0;                            /* the wet top highlight */
  --wt-pad:   13px 15px 14px;
  --wt-pill:  rgba(0, 0, 0, 0);             /* the delta's badge */
  --wt-heart: 0;                            /* the little heart by the label */
  --wt-kt:    0.08em;                       /* the label's tracking */
  --wt-kc:    uppercase;

  /* ⚠ NO `isolation: isolate` HERE, however much the gloss layer looks like it wants
     one. The fuzz below is a `z-index: -1` child, and a stacking context on this card
     puts a negative child ON TOP of the card's own background instead of behind it —
     so the soft halo becomes a blurred pink wash lying across the widget. The order
     comes out right without one: the fuzz behind, the card, the gloss at 0, the
     contents at 1. */
  align-self: flex-start; position: relative;
  width: min(100%, 268px);
  padding: var(--wt-pad);
  border-radius: var(--wt-r);
  background: linear-gradient(160deg, var(--wt-bg), var(--wt-bg2));
  color: var(--wt-ink);
  box-shadow: var(--wt-glow);
  transition:
    border-radius 620ms cubic-bezier(0.34, 1.3, 0.5, 1),
    background    620ms ease,
    color         620ms ease,
    padding       620ms ease,
    box-shadow    620ms ease;
}
/* ⚠ THE GLOSS IS THE SINGLE BIGGEST THING THAT MAKES IT CUTE, and it is one gradient.
   A flat pink rectangle is a grey widget that has been painted; a pink one with light
   sitting on the top of it is a soft OBJECT, which is the whole difference between
   "recoloured" and "cute". It is a `::before` rather than a second background layer
   because the card's background is already a gradient the skin transitions, and a
   `background` shorthand carrying two images cannot be crossfaded between skins.
   ⚠ AND IT MUST NOT PAINT OVER THE CONTENTS: a positioned `z-index: 0` layer paints
   after in-flow text, so the three rows below are lifted to 1. */
.wt::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  border-radius: inherit;
  /* ⚠ 0.5 TO NOTHING BY 46%, AND BOTH NUMBERS CAME DOWN. At 0.78 running to 58% the
     highlight bleached the whole top third: the card's own gradient already starts
     near-white up there, so the label, the badge and the pill's white background all
     landed on white and the thing read as a WHITE widget with a pink chart in it —
     which is the one thing a card asked to be pink may not do. A gloss is a
     reflection on a coloured surface; you have to be able to see the surface. */
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 46%);
  opacity: var(--wt-gloss);
  transition: opacity 620ms ease;
}
.wt-top, .wt-n, .wt-bars { position: relative; z-index: 1; }
/* the fuzz. ⚠ A SEPARATE LAYER, because the card's own contents must stay sharp —
   fluffy is a quality of the EDGE, and blurring the type would just be out of focus. */
.wt::after {
  content: ""; position: absolute; inset: -7px; z-index: -1;
  border-radius: calc(var(--wt-r) + 7px);
  background: var(--wt-bg2);
  filter: blur(9px);
  opacity: var(--wt-fuzz);
  transition: opacity 620ms ease, border-radius 620ms ease, background 620ms ease;
}
/* ⚠ CENTRE, NOT BASELINE, once the delta became a badge. A pill baseline-aligned with
   a line of small caps hangs its own padding below the text and sits visibly low. */
.wt-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
/* ⚠ THE LABEL COMES OUT OF SMALL CAPS IN THE CUTE SKIN. Tracked uppercase is the one
   typographic tic that says DASHBOARD, and no amount of pink gets a dashboard to read
   as cute — so `--wt-kt`/`--wt-kc` are tokens like every other difference. */
.wt-k {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 11px; font-weight: 600;
  letter-spacing: var(--wt-kt); text-transform: var(--wt-kc);
  color: var(--wt-mute);
  transition: color 620ms ease, letter-spacing 620ms ease, font-size 620ms ease;
}
.wt-heart {
  width: 11px; height: 11px; flex: none; display: block;
  fill: var(--wt-hi);
  opacity: var(--wt-heart);
  transition: opacity 620ms ease, fill 620ms ease;
}
/* the delta, as a badge it can transition INTO — a naked figure in the plain skin
   because the pill's own background is a token that starts fully transparent */
.wt-d {
  display: inline-flex; align-items: center; gap: 3px;
  padding: var(--wt-dp, 0);
  border-radius: 999px;
  background: var(--wt-pill);
  font-size: 11.5px; font-weight: 600; color: var(--wt-hi);
  white-space: nowrap;
  transition: color 620ms ease, background 620ms ease, padding 620ms ease;
}
.wt-fall { width: 10px; height: 10px; flex: none; display: block; opacity: var(--wt-heart); transition: opacity 620ms ease; }
.wt-n {
  margin-top: 3px;
  font-size: 36px; line-height: 1.02; letter-spacing: -0.035em;
  font-weight: var(--wt-w);
  /* ⚠ PROPORTIONAL, NOT TABULAR. Tabular figures give the FULL STOP a whole digit's
     width, so `72.4` set at 36px came out as `72 . 4` with a hole either side of the
     point. Tabular is for a column of numbers that has to line up; this is one
     number, and the only thing the setting bought was the gap. */
  font-variant-numeric: proportional-nums;
  transition: font-weight 620ms ease, letter-spacing 620ms ease;
}
/* ⚠ THE UNIT KEEPS ITS OWN SPACE RATHER THAN A LEADING SPACE IN THE STRING. It used
   to be `<span> lb</span>` and a space set at 15px beside a 36px number is not the
   gap anybody wants; a margin is a number that can be tuned. */
.wt-u { margin-left: 3px; font-size: 15px; font-weight: 600; letter-spacing: -0.01em; color: var(--wt-mute); transition: color 620ms ease; }
/* ⚠ 52, AND THE RANGE OF THE DATA MATTERS AS MUCH AS THE HEIGHT. At 38px with
   heights between 41 and 61 per cent, a bar was about as tall as it was wide and
   every one of the fourteen came out a CIRCLE once the fluffy skin rounded them —
   fourteen pink dots in a row, which is decoration rather than a month of weight.
   A chart has to survive being made fluffy or the screen is demonstrating that a
   re-skin destroys the app. */
.wt-bars { display: flex; align-items: flex-end; gap: var(--wt-bg-gap, 3px); height: 52px; margin-top: 11px; }
/* ⚠ A GRADIENT PER BAR, NOT A FLAT FILL, and it is what turns fourteen pink sticks
   into fourteen soft ones. Both stops are tokens so the plain skin's gradient runs
   between two copies of the same grey, i.e. is a flat fill and costs nothing. */
.wt-bars i {
  flex: 1; display: block; min-width: 0; position: relative;
  background: linear-gradient(180deg, var(--wt-bar2), var(--wt-bar));
  border-radius: var(--wt-br);
  transition: background 620ms ease, border-radius 620ms cubic-bezier(0.34, 1.3, 0.5, 1), height 620ms cubic-bezier(0.34, 1.2, 0.5, 1);
}
.wt-bars i:last-child { background: linear-gradient(180deg, var(--wt-hi2), var(--wt-hi)); }
/* today's pin. ⚠ IT IS ON THE BAR AND SITS PROUD OF IT rather than being a
   fifteenth element in the row, so nothing about the chart's geometry changes and a
   plain skin simply has it at zero. */
/* ⚠ THERE IS NO PIN OVER TODAY'S BAR, AND IT WAS TRIED TWICE. Floated clear of the
   top, a dot with a stick under it reads as a lowercase i; sat ON the bar with a ring
   of the card's colour round it, the ring reads as a hole punched through the bar.
   Both were drawn and looked at. The last bar is already the only saturated one in
   the row, which says "today" without a legend and without a second object — and on
   a brief that says CLEAN, the marker that needs explaining is the one to cut. */

/* ── pink, and CUTE ──────────────────────────────────────────────────────────
   ⚠ yulichkus, 2026-09-08: "can we actually take time to make it cute please?" It
   was a grey widget with a pink coat of paint — same 14px-ish corners, same tracked
   small caps, same flat bars, all of it recoloured — and recoloured is exactly what
   it read as.

   ⚠ CUTE HERE IS SHAPE AND SURFACE, and that is not a compromise, it is what
   "pink and clean" leaves you: fat corners, light sitting on the top of the card,
   chunky round bars that step from pale to hot, a softer label, a badge instead of a
   loose figure, one small heart and one small pin. No stickers, no face, no second
   accent colour — every one of those was available and every one of them is the
   version that would have needed the copy to say "cute" instead of "clean".

   ⚠ AND ALL OF IT IS TOKENS, so the animated loop still travels from one design to
   the other in front of you rather than swapping two pictures. That constraint is
   what stopped this being a second card: anything that could not be expressed as a
   number the plain skin also has does not appear. */
.wt[data-skin='fluffy'] {
  /* ⚠ THE TOP OF THE CARD IS PINK, NOT NEARLY WHITE — see the gloss's own note. */
  --wt-bg:    #FFE6F1;
  --wt-bg2:   #FFD0E5;
  --wt-ink:   #5E1F3E;
  --wt-mute:  #C07EA3;
  /* ⚠ THE PALE END IS THE BOTTOM. A bar lit at the top and pale at the foot reads as
     a soft solid; the other way round it reads as a bar that is fading out. */
  /* ⚠ AND THE FOOT OF A BAR HAS TO CLEAR THE CARD UNDER IT. At #FFC7DF against a
     #FFD0E5 card the bars dissolved into the background at exactly the point they are
     standing on, so the chart had no baseline and read as fourteen floating lozenges. */
  --wt-bar:   #FFB9D6;
  --wt-bar2:  #FF9DC6;
  --wt-hi:    #F4589E;
  --wt-hi2:   #FF7FB8;
  /* ⚠ 30 ON A 268-WIDE CARD IS AS FAR AS IT GOES. At 36 the corner arcs run into the
     `Weight` label's own left edge and the card stops reading as a rectangle with
     soft corners and starts reading as a pill somebody put a chart in. */
  --wt-r:     30px;
  --wt-br:    999px;
  --wt-w:     800;
  /* ⚠ THE HALO FOLLOWS THE WORD, AND IT HAS NOW MOVED TWICE. The ask said "pink and
     CLEAN" on the 8th and this came down to 0.28 — a hint of bloom under a real drop
     shadow, because a soft edge on a card reads as an object somebody has not finished
     rendering, which is the opposite of clean. The ask says FLUFFY again (Joonas,
     2026-09-09), so it is back up: nine pixels of blurred pink round the card is
     exactly what the sentence is asking for, and a card asked to be fluffy that is not
     fluffy is the screen failing its own demonstration. */
  --wt-glow:  0 12px 26px -14px rgba(244, 88, 158, 0.46);
  --wt-fuzz:  0.85;
  --wt-gloss: 1;
  --wt-pad:   16px 18px 17px;
  --wt-pill:  rgba(255, 255, 255, 0.72);
  --wt-heart: 1;
  --wt-kt:    -0.006em;
  --wt-kc:    none;
  --wt-dp:    3px 8px;
  --wt-bg-gap: 4px;
}
/* ⚠ ONE SIZE CHANGE, AND IT IS THE LABEL. Out of small caps at 11px "Weight" is
   noticeably smaller than everything round it, because tracked uppercase occupies far
   more width than its point size suggests. */
.wt[data-skin='fluffy'] .wt-k { font-size: 12.5px; font-weight: 650; }

/* ── visual three: the app that was made, behind glass ───────────────────── */
/* ⚠ IT IS A BOX AND THE OTHER TWO ARE NOT, because this one is an OBJECT — a thing
   that exists, is finished, and is being held back. A chat is a surface; a thing
   behind glass is a card. */
.vprev {
  position: relative; flex: none; overflow: hidden;
  border-radius: var(--r-lg);
  border: 1px solid var(--hair-2);
  background: var(--fill);
  padding: 16px;
  display: flex; align-items: center; gap: 15px;
}
.vprev .vw { width: 104px; height: 104px; border-radius: 22px; }
/* ⚠ THE GLASS THAT USED TO RUN ACROSS THIS IS GONE — Joonas, 2026-09-06: "we don't
   need blur as it will be pre-set". It was arguing that there is more of this and you
   cannot have it yet. The screen now says the tool is BUILT and waiting on your home
   screen, and a blur over something you have already been given is not a tease, it is
   an out-of-focus picture of your own app. */
.vprev-txt { position: relative; z-index: 2; min-width: 0; }
.vprev-txt b { display: block; font-size: 16px; font-weight: 600; letter-spacing: -0.018em; }
/* ⚠ THE NOTE HAS ITS OWN CLASS, and it needs one. Written as `.vprev-txt span` this
   rule is specificity (0,1,1) and `.vprev-seal` is (0,1,0), so the DESCENDANT
   selector won and the seal — an `inline-flex` pill meant to hug two words — was
   `display: block` and ran the full width of the column as a pale pink bar. The
   sibling that steals a declaration is invisible in either rule on its own. */
.vprev-note { display: block; margin-top: 3px; font-size: 12.5px; line-height: 1.4; color: var(--muted); }
.vprev-seal {
  position: relative; z-index: 2; margin-top: 9px;
  display: inline-flex; align-items: center; gap: 6px;
  border-radius: 999px; padding: 4px 10px 4px 8px;
  background: var(--accent-soft); color: var(--accent-ink);
  font-size: 11px; font-weight: 600; letter-spacing: 0.02em;
}
.vprev-seal svg { width: 11px; height: 11px; display: block; fill: currentColor; }

/* ══ REDUCED MOTION ═════════════════════════════════════════════════════════════
   ⚠ THE MECHANISM STAYS, ONLY THE CLOCK STOPS. The loop still shows its four beats
   and the tracker still re-skins — a reader who asked for calm gets the same
   information, not less of it — they simply arrive rather than travelling. */
@media (prefers-reduced-motion: reduce) {
  .scrn.on, .vin, .vgrow, .qr button, .done.on { animation-duration: 1ms !important; animation-delay: 0ms !important; }
  .scrn.on .head s::after { animation-duration: 1ms !important; animation-delay: 0ms !important; }
  .wt, .wt::before, .wt::after, .wt-n, .wt-bars i,
  .wt-k, .wt-heart, .wt-d, .wt-fall, .wt-u { transition-duration: 1ms !important; }
  .typing i { animation: none; opacity: 0.5; }
  .cta, .chip, .pick { transition-duration: 1ms !important; }
}

/* ══ TWO LATE RULES ═════════════════════════════════════════════════════════════ */

/* the box a visual is built into. It has no look of its own — the visuals draw
   themselves — but it must not be allowed to flex, or a tall thread squeezes the
   headline under it. */
/* ⚠ THE MIN-HEIGHT BELONGS TO THIS BOX AND THE VISUAL INSIDE IT MUST BE ABLE TO
   SHRINK, and getting that backwards made the demonstration PAINT OVER THE BUTTON.
   The host was `flex: 1 1 auto; min-height: 0` — squeezable to nothing — while the
   thread inside it held a `min-height: 208px`, so on a 320x568 phone the host was
   given 150px, the thread stayed 208, and the 58px it could not fit came out the
   bottom on top of "Ask away". The host clips and carries the floor; the visual
   fills whatever the host got. */
.vhost { flex: none; display: block; overflow: hidden; }
/* ⚠ AND ON s1 IT GROWS, because the wall fills the band rather than sitting in the top
   of it — see the wall's own note. Left at `flex: none` the picture was 224px tall in a
   590px band and the whole difference pooled between it and the headline. */
.scrn[data-id='s1'] .vhost { flex: 1 1 auto; min-height: 0; }

/* ⚠ THE THREAD SCROLLS AND THE COMPOSER DOES NOT. Every other screen is one scroller
   with a button at the end of it; a chat is the one shape where the newest turn has
   to stay in view while the field stays under your thumb, so this screen does not
   scroll as a whole and the thread inside it does. */
.scrn[data-id='chat'] { overflow: hidden; }
.scrn[data-id='chat'] .thread {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  padding-bottom: 2px;
}
/* ⚠ BOTTOM-ALIGNED VIA `margin-top: auto` ON THE FIRST TURN, never
   `justify-content: flex-end`. On a scroller that centres or end-aligns, content
   taller than the box overflows UPWARD and the part above the start is unreachable —
   you cannot scroll back to the first thing that was said. The margin gives the same
   composition when the thread is short and gets out of the way when it is not. */
.scrn[data-id='chat'] .thread > :first-child { margin-top: auto; }
.scrn[data-id='chat'] .thread::-webkit-scrollbar { width: 0; }
.scrn[data-id='chat'] .spacer { display: none; }


/* ══ THE LANDING'S COMPOSITION ══════════════════════════════════════════════════
   ⚠ THE VISUAL GROWS AND THE TYPE DOES NOT. This screen is a picture over a
   sentence over a button, and the only one of the three that can absorb a hundred
   pixels of a taller phone without looking wrong is the picture. Given the free
   space to a spacer instead, the type and the button drift apart and the screen
   reads as two unrelated halves. */
.scrn[data-id='s1'] { padding-bottom: max(40px, calc(env(safe-area-inset-bottom) + 20px)); }
/* ⚠ CAPPED, AND THE SPACER AFTER IT IS WHAT TURNS THE CUT INTO AIR. Left to fill,
   the thread ran 549px on a 390 phone and its last bubble sat 20px under the
   headline — every bit of room on the screen was inside the demo. The cap is a
   `cqw` because this app is a width-container and `vh` would be the WINDOW, which
   inside the desktop frame is the wrong box entirely; measured, 94cqw takes the
   visual down about a third at every phone size and a quarter inside the frame. */
.scrn[data-id='s1'] .vhost {
  /* ⚠ THE CAP IS GONE, AND IT WAS THE 394 NOBODY COULD EXPLAIN. `max-height: 98cqw`
     was right for the visual that used to be here — a chat still, two bubbles and a
     card, which given the whole band looked stranded in the middle of it — and it is
     wrong for a CROP of a home screen, whose entire idea is that it runs past the
     frame. With the cap on, the wall stopped at 394px whatever the phone and the
     difference pooled between it and the headline as a third of a screen of white;
     three attempts to fix that by changing the WALL moved nothing, because the wall
     was already `height: 100%` of a box that was capped.
     ⚠ AND THE GAP BELOW IT IS FIXED NOW, so nothing else is competing for the band:
     the picture takes it and the mask decides where it ends. */
  flex: 3 1 auto; min-height: 150px;
  display: flex; flex-direction: column;
}
.scrn[data-id='s1'] .head { margin-top: 20px; }
.scrn[data-id='s1'] .cta  { margin-top: 20px; }

/* ⚠ THIS ONE HUGS ITS CONTENT AND THE LOOP DOES NOT, and the difference is what the
   two visuals ARE. The loop is a thread that keeps arriving, so it should take the
   room it is given; the tracker is a fixed four-turn composition, so a box taller
   than it holds is a bottom-aligned demo with a hundred and thirty pixels of empty
   box above it — which, on top of the gap below the copy, read as a hole in the
   middle of the screen rather than as air. `flex: 0 1 auto` sizes it to the demo and
   lets the gap own all of the free space. */
.scrn[data-id='s2'] .vhost {
  flex: 0 1 auto; min-height: 0; max-height: 98cqw;
  display: flex; flex-direction: column;
}

/* ⚠ THE PREVIEW STACKS ON A SMALL PHONE. At 320 the card is 276px wide and a 104px
   widget leaves 160 for a name, two lines of note and a seal — the note ran to four
   lines and the seal sat on top of the last one. Side by side is the better
   arrangement wherever there is room for it, and there is not room for it here. */
@media (max-width: 379px) {
  .vprev { flex-direction: column; align-items: flex-start; gap: 12px; }
  .vprev .vw { width: 84px; height: 84px; border-radius: 18px; }
}

/* the gap the cap frees up — it grows on a tall phone and collapses on a short one */
.scrn[data-id='s1'] .gap, .scrn[data-id='s2'] .gap { flex: 2 1 auto; min-height: 12px; }
/* ⚠ EXCEPT ON s1, WHERE THE PICTURE TAKES THE BAND. This gap is `flex: 2` — it claims
   twice the free height the visual does — which is right for screen two, whose demo
   hugs two objects and would look stranded made tall. Screen one's wall is a CROP of a
   home screen and grows into whatever it is given, so leaving the gap elastic split
   the band and left 250px of white between a picture that fades out and a headline. */
.scrn[data-id='s1'] .gap { flex: 0 0 22px; min-height: 22px; }
.gap--sm { flex: 1 1 auto; min-height: 8px; }

/* ══ A SHORT PHONE, AND NOTHING MAY SCROLL ══════════════════════════════════════
   ⚠ JOONAS, 2026-09-06: "make sure that everything fits into 1 page of the mobile
   screen, no scrolling ever (except for chat) — even on mobile web."
   MEASURED before this block: at 320x568 (the SE, the shortest phone anyone still
   carries) q1 ran 79px past the fold, q2 56, q3 103, q5 116 and the cards screen
   THREE HUNDRED AND TWENTY THREE. Every other size fits — 375x812 and up had room
   already — so this is a HEIGHT query and not a width one: the fault is vertical and
   a narrow-but-tall phone must not be squeezed for it.
   ⚠ AND THE TAP TARGETS SURVIVE IT. Nothing here goes under 44px: the chips lose six
   pixels of the 52 they were given (52 was chosen over 44 for comfort, not for
   reach), the padding inside them comes down, and the rest of the saving is taken
   out of AIR — the note's margin, the dots' margin, the question's leading — because
   air is what a short screen has least of and what costs nothing to give up. */
@media (max-height: 700px) {
  .scrn { padding-bottom: max(20px, calc(env(safe-area-inset-bottom) + 12px)); }
  .dots { margin: 6px 0 14px; }
  .q { font-size: clamp(19px, 5.4cqw, 22px); margin-bottom: 6px; }
  .note { margin-bottom: 12px; }
  .sub { font-size: 13.5px; margin-top: 6px; }
  .choices { gap: 6px; }
  .chip { min-height: 46px; padding: 9px 13px; font-size: 14.5px; }
  .cta { min-height: 46px; margin-top: 10px; }
  .head { font-size: clamp(23px, 7cqw, 28px); }

  /* the cards screen is the worst of them, and it is carrying a design control */
  /* ⚠ q5 IS THE LONGEST QUESTION — six answers at 46px plus five 6px gaps is 306px,
     which left it TWO pixels over. A pixel off each gap is the whole fix. */
  .choices { gap: 5px; }

  /* ⚠ THE CARDS SCREEN IS THE WORST OF THEM AND ITS HEIGHT IS THE COPY, NOT THE ART.
     MEASURED at 320x568: three cards came to 455px of a 498px screen, and inside each
     one the tile is 44px while the TEXT is 110 to 146 — the description wraps to three
     and four lines at that width. So the description is clamped to two lines here and
     nowhere else. It is the supporting line on a card whose title already says what
     the thing is, and two lines of it is still a sentence; the alternative was taking
     a card off the screen, which is the one thing this screen cannot lose. */
  .picks { gap: 7px; }
  .pick { padding: 11px; border-radius: 16px; gap: 11px; }
  .pick .ptxt b { font-size: 14.5px; }
  /* ⚠ AND ON THE SHORTEST PHONE THE DESCRIPTION GOES ENTIRELY. Clamping it to two
     lines was tried first and saved NOTHING — MEASURED, it was already two lines, and
     the height is the three titles plus three descriptions plus three badges. Three
     descriptions are ~105px of a screen that is 96px over, and each card's TITLE is
     already a full sentence saying what the thing is ("Get this term's pickups and
     classes in one calendar"), so the choice survives losing the gloss. Taking a
     third option off the screen was the alternative, and this screen is the one that
     cannot lose one. */
  .pick .ptxt .pd { display: none; }
  /* ⚠ AND IT SHRINKS WITH THE CARD IT IS ON. The description is dropped at this width,
     so the card is two lines rather than three and a 60px tile is taller than the type
     beside it — which puts the widget in charge of a row whose job is a sentence. */
  .pick .vw { width: 48px; height: 48px; border-radius: 12px; }
  .scrn[data-id='cards'] .q { margin-bottom: 2px !important; font-size: 18px; line-height: 1.1; }
  /* ⚠ THE SPACER STAYS FLEXIBLE. It was pinned to 4px to buy the last pixels on this
     screen when the card-style pill was still on it; the pill was deleted on
     2026-09-06 and that gave sixty back, so pinning it now just parks the ghost
     button in the middle of the screen with a third of the page empty under it. */
  .scrn[data-id='cards'] .spacer { flex: 1 1 auto; min-height: 4px; }
  /* ⚠ THE CARD-STYLE PICKER STANDS DOWN. It is a design control — the same standing
     as the landing page's backdrop switch — and on the one screen that is 323px over
     the fold, sixty pixels of chrome asking which card style you prefer is the first
     thing that should go. It is still there on every phone with the room for it. */
  /* ⚠ THE GHOST KEEPS ITS 44. It is a real control — "I'll do this later", "None of
     these" — and the first version of this block took it to 40 and then 38 to buy the
     last pixels on the cards screen. The suite caught it on two screens at two sizes:
     nothing on a phone goes under 44, and the pixels have to come out of air instead. */
  .ghost { min-height: 44px; margin-top: 4px; }
}
