/* ==========================================================================
   Factbox — the quiz onboarding engine. The stylesheet for js/onboard.js.

   WHAT THIS FILE IS NOW. A port of journey 3a of the Quiz Funnel mockup,
   screen for screen. The mockup draws inside a 390x844 phone frame and every
   offset in it is absolute against that frame, so this file does the same:
   .fbob IS the frame. Sizes below are the mockup's own literals, in px,
   because the ladder in css/app.css is a clamp() ladder for pages that reflow
   and these screens do not reflow — they are a phone.

   THE FRAME. .fbob is position:absolute against .fbg-sheet.fbg-quiz, which
   css/recommend.css already makes top:0, full height, no radius. Absolutely
   positioned children resolve against a PADDING BOX, so inset:0 here covers
   the host's own 14/20/bottom-safe padding as well and the port gets the
   whole 390x844 rather than an inset rectangle. That is what makes screen 1's
   hero full-bleed without this file touching recommend.css's padding.

   COLOUR, AND WHY THE TOKENS ARE RE-DECLARED. The funnel is CREAM: #E7E0D3
   ground, #172E5B ink, #3B9EF4 fills. Those are css/app.css's --ground, --ink
   and --coral exactly. But this flow is mounted on the reader, and
   css/reader-rail.css repaints :root for night — --ground becomes #090C16 and
   --ink becomes cream. Rendering the funnel through those tokens is precisely
   the inversion the owner rejected. So the paper palette is re-declared at
   the one scope this file owns, .fbob, with app.css's own values. Below that
   line every rule is a var().

   The literals this file is allowed, and they are the mockup's:
     #2B0E13         the ground behind a painting, and screen 1's ground
     #F4EFE6         type on a painting (= app.css --on-glass)
     rgba(19,6,9,·)  the four art scrims
     rgba(43,14,19,·) screen 1's two gradients
   ========================================================================== */

.fbob{
  /* ---- the paper palette, re-declared. Values are css/app.css's. ---- */
  --ground:#E7E0D3;
  --raise:#F1EBDF;
  --ink:#172E5B;
  --dim:rgba(23,46,91,.82);
  --dimmer:rgba(23,46,91,.74);
  --hair:rgba(23,46,91,.14);
  --tint-2:rgba(23,46,91,.12);
  --tint-3:rgba(23,46,91,.18);
  --accent-wash:rgba(59,158,244,.12);
  --accent-edge:rgba(59,158,244,.5);
  --coral:#3B9EF4;
  --coral-deep:#1B5FA8;
  --on-accent:#06182E;
  --ring:#1A5BA1;

  /* the mockup's own two, which app.css has no token for */
  --plate:#2B0E13;
  --on-plate:#F4EFE6;

  /* The mockup pins its buttons at bottom:26 / bottom:30 of an 844px frame.
     On a phone with a home indicator that band is not free, so the indicator
     is added to it and nothing else is. */
  --ob-safe:env(safe-area-inset-bottom, 0px);

  position:absolute; top:0; right:0; bottom:0; left:0;
  overflow:hidden;
  background:var(--ground);
  color:var(--ink);
  font-family:var(--ui);
  font-size:16px;
  text-align:left;
  -webkit-font-smoothing:antialiased;
}

/* ==========================================================================
   WIDE SCREENS: the funnel is a phone-shaped column, not a stretched page.

   Every offset in this file comes from the mockup's 390x844 frame — the hero
   art is 600 tall, the loader's cover card sits at top:474, the buttons are
   pinned 26-30px off the bottom. Those are absolute positions inside a box
   that box was 390x844.

   Let that box become the browser window and all of it comes apart: on a
   2000px monitor the two story tiles were a thousand pixels wide with their
   paintings cropped to letterbox strips, "Yes / Not sure" stretched into a
   pair of runways, and every screen ended in a third of a page of empty
   cream, because content anchored to top:474 of an 844 frame has nowhere to
   go in a 1200px one.

   So the frame stays a frame. `margin:auto` with all four offsets at 0 and a
   definite size centres it on both axes — no transform, deliberately, because
   a transformed ancestor would become the containing block for the plate
   morph's own transforms and that is a debugging session nobody needs.

   Below 520px nothing here applies and the funnel is edge to edge, which is
   where it is actually read.
   ========================================================================== */
@media (min-width:520px){
  .fbg.is-quiz .fbg-sheet.fbg-quiz{
    background:var(--night, #0F1420);   /* the surround, so the column reads as a device */
    padding:0;
  }
  .fbob{
    width:100%;  max-width:440px;
    height:100%; max-height:880px;
    margin:auto;                        /* with top/right/bottom/left all 0 */
    border-radius:22px;
    overflow:hidden;
    box-shadow:0 24px 60px rgba(0,0,0,.45);
  }
}
/* A short window is not a phone: let it use the height it has rather than
   clipping the button off the bottom of a laptop in landscape. */
@media (min-width:520px) and (max-height:900px){
  .fbob{max-height:100%; border-radius:0}
}

/* ==========================================================================
   WHY THESE NUMBERS, AND WHY TWO GROUPS OF THEM.

   The owner watched a screen recording and said the motion was "a little
   fast". Measuring the recording agreed and said where: frame differencing
   across a transition showed the screen swapping in a SINGLE frame — one
   delta of 559 parts per thousand — and then a decaying tail of motion that
   settled by 600ms. A 260ms whole-screen fade is short enough that it reads
   as a cut with some movement after it, rather than as one screen becoming
   another.

   So the entrances were lengthened and the FEEDBACK WAS NOT. That split is
   the whole idea:

     ENTRANCES may be leisurely. Nobody is waiting on them — the reader has
     just arrived and the motion is what tells them something new is here.
     Screen fade 260 -> 400. Words 460 -> 580 at a 75ms stagger rather than
     55. Rows 380 -> 480. Plates 520 -> 660.

     FEEDBACK MUST BE INSTANT. The tap spring (340ms), the tick pop (380ms)
     and the 160ms colour transitions are untouched. A control that takes
     half a second to admit it was pressed does not feel considered, it feels
     broken, and the reader's finger is still on the screen waiting for it.

   The plate morph at 520ms in js/recommend.js is also untouched: it is not
   an entrance, it is one object moving between two places, and its duration
   is tied to the distance it travels.
   ========================================================================== */

/* ==========================================================================
   THE MOTION SYSTEM.

   THE ONE RULE EVERYTHING BELOW OBEYS: transform and opacity, and nothing
   else. The two bars used to transition `width` and are scaleX() now. The one
   exception is the 6px blur a headline word resolves out of, which is the
   whole of the reference's typographic language and is short. Everything is
   composited; nothing in this funnel triggers layout on a mid-range phone in
   an in-app browser, which is where all of it is actually seen.

   THE SECOND RULE: NOTHING STICKS. Every keyframe below both starts and ENDS
   at the resting state and every one runs with fill-mode both, so an element
   whose animation is interrupted — a fast tapper is four screens away before
   the first stagger finished — is left visible rather than at opacity 0.
   There is no keyframe here that ends anywhere but at rest.

   WHERE THE CLASSES COME FROM. js/onboard.js marks an element for entry ONLY
   on the render that first puts it on screen: `ob-in`, `ob-in-row`,
   `ob-in-deal`, `ob-in-art`, and `is-in` on a headline word. A repaint — a
   tick being ticked, the loader redrawing itself three times a second — gets
   no class and therefore no animation, which is why tapping an option does
   not flash the phone.
   ========================================================================== */

/* the whole-screen arrival. It stands down when a plate is MORPHING across
   the join: the point of a morph is that one element is continuous, and a
   parent fading 0→1 underneath it makes it a dissolve again. */
@keyframes ob-fade{ from{opacity:0} to{opacity:1} }
.fbob .ob-screen{
  position:absolute; top:0; right:0; bottom:0; left:0;
}
.fbob .ob-screen.is-enter{animation:ob-fade .40s ease both}
.fbob .ob-screen.is-enter.is-morph{animation:none}

/* the four entry moves */
@keyframes ob-rise{
  from{opacity:0; transform:translate3d(0,12px,0)}
  to{opacity:1; transform:none}
}
@keyframes ob-row{
  from{opacity:0; transform:translate3d(0,18px,0) scale(.98)}
  to{opacity:1; transform:none}
}
@keyframes ob-deal{
  from{opacity:0; transform:translate3d(0,26px,0) rotate(-1.2deg)}
  to{opacity:1; transform:none}
}
@keyframes ob-art{
  from{opacity:0; transform:scale(1.035)}
  to{opacity:1; transform:none}
}
.fbob .ob-in     {animation:ob-rise .54s cubic-bezier(.2,.75,.25,1) both}
.fbob .ob-in-row {animation:ob-row  .48s cubic-bezier(.2,.75,.25,1) both}
.fbob .ob-in-deal{animation:ob-deal .58s cubic-bezier(.2,.75,.25,1) both}
.fbob .ob-in-art {animation:ob-art  .66s cubic-bezier(.2,.75,.25,1) both}

/* ---- A · the word-by-word headline -------------------------------------
   The spans are inline-block so a word can never break across a line, and
   the whitespace between them is real text nodes so the line still wraps
   where the browser would have wrapped it. The heading's text is unchanged
   and a screen reader reads one string. */
.fbob .ob-w{display:inline-block}
@keyframes ob-word{
  from{opacity:0; transform:translate3d(0,14px,0); filter:blur(6px)}
  to  {opacity:1; transform:none;                  filter:blur(0)}
}
/* will-change ONLY while it is animating. A headline is up to a dozen words
   and a dozen permanent composited layers on a phone is a cost with no
   payment; the class is removed from the markup on the next render anyway. */
.fbob .ob-w.is-in{
  animation:ob-word .58s cubic-bezier(.2,.75,.25,1) both;
  will-change:opacity, transform, filter;
}

/* ---- B · the plate that travels -----------------------------------------
   The outgoing painting, riding inside the incoming plate under the same
   aspect correction, fading out as the frame arrives. js/onboard.js owns
   every transform on it; this rule only says where it sits and that it is
   above the picture it is dissolving into. */
.fbob .ob-ghostplate{
  position:absolute; top:0; right:0; bottom:0; left:0;
  display:block; width:100%; height:100%;
  object-fit:cover; object-position:center 26%;
  pointer-events:none;
}

/* the mockup's .fbscroll: it scrolls, it shows no bar */
.fbob .ob-scroll{
  overflow-y:auto; overflow-x:hidden;
  overscroll-behavior:contain;
  -webkit-overflow-scrolling:touch;
  scrollbar-width:none;
}
.fbob .ob-scroll::-webkit-scrollbar{width:0; height:0}

/* ==========================================================================
   THE PRIMARY BUTTON. One declaration, four screens.
   mockup: 16/20 padding, 52 min-height, 12 radius, 15px 700 .06em uppercase.
   ========================================================================== */
.fbob .ob-cta{
  -webkit-appearance:none; appearance:none;
  display:block; width:100%;
  padding:16px 20px; min-height:52px;
  border:0; border-radius:12px;
  background:var(--coral); color:var(--on-accent);
  box-shadow:0 8px 22px rgba(27,95,168,.26);
  font-family:var(--ui); font-size:15px; font-weight:700;
  letter-spacing:.06em; text-transform:uppercase; line-height:1.2;
  cursor:pointer;
}
/* Off, not hidden. The mockup's CTA_OFF: it says it is waiting. */
.fbob .ob-cta.is-off{
  background:rgba(23,46,91,.1); color:rgba(23,46,91,.42);
  box-shadow:none; cursor:default;
}

/* ==========================================================================
   CHROME. The mockup's fixed top bar: a 38px round back arrow, the centred
   logo and wordmark, and three phase bars with labels underneath.
   ========================================================================== */
.fbob .ob-top{
  position:absolute; top:0; left:0; right:0; z-index:20;
  padding:16px 18px 12px;
  background:var(--ground);
}
.fbob .ob-toprow{
  display:flex; align-items:center; gap:10px;
  min-height:38px;
}
.fbob .ob-back{
  -webkit-appearance:none; appearance:none;
  flex:0 0 auto;
  width:38px; height:38px; min-height:38px;
  display:inline-flex; align-items:center; justify-content:center;
  padding:0; border:0; border-radius:999px;
  background:none; color:var(--ink);
  font-family:var(--ui); font-size:17px; line-height:1;
  cursor:pointer;
}
.fbob .ob-mark{
  flex:1 1 auto;
  display:flex; align-items:center; justify-content:center; gap:7px;
}
.fbob .ob-mark img{display:block}
.fbob .ob-markw{
  font-size:12px; font-weight:700; letter-spacing:.3em;
  color:var(--coral-deep);
}
.fbob .ob-topgap{flex:0 0 auto; width:38px}

.fbob .ob-phases{display:flex; gap:6px; margin:12px 0 0}
.fbob .ob-phase{flex:1 1 0; min-width:0}
.fbob .ob-track{
  height:4px; border-radius:999px; overflow:hidden;
  background:var(--hair);
}
/* D · scaleX, not width. js/onboard.js sets the transform and, on arrival,
   the 420ms transition that carries it from the last screen's percentage to
   this one's — so the bar is seen to advance rather than to be already
   advanced. A width transition would relayout the bar on every frame. */
.fbob .ob-fill{
  display:block; height:100%; width:100%;
  transform:scaleX(0); transform-origin:0 50%;
  border-radius:999px; background:var(--coral);
}
.fbob .ob-plabel{
  margin:7px 0 0; text-align:center;
  font-size:11px; font-weight:700; letter-spacing:.1em;
  text-transform:uppercase; color:var(--dimmer);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.fbob .ob-phase.is-on .ob-plabel{color:var(--coral-deep)}

/* ==========================================================================
   PLATES. Every painting in this file is an <img> inside a positioned box, so
   a 404 has a fallback to swap to (js/onboard.js sets data-fallback) and the
   box keeps the dark ground underneath while it decodes.
   ========================================================================== */
.fbob .ob-plate{
  position:absolute; top:0; right:0; bottom:0; left:0;
  background:var(--plate);
}
.fbob .ob-plate img{
  display:block; width:100%; height:100%;
  object-fit:cover; object-position:center 26%;
}
.fbob .ob-scrim{position:absolute; top:0; right:0; bottom:0; left:0}

/* ==========================================================================
   1 · WELCOME. The mockup's is1: full-bleed painting to 600px, a gradient
   floor from 396px, the wordmark, a 44px serif headline, the lede, Continue
   at bottom:30 and the terms line under it.
   ========================================================================== */
.fbob .ob-welcome{background:var(--plate)}
.fbob .ob-w-art{
  position:absolute; top:0; left:0; right:0; height:600px;
  overflow:hidden;
}
.fbob .ob-w-art .ob-plate img{object-position:center 22%}
.fbob .ob-w-artscrim{
  position:absolute; top:0; right:0; bottom:0; left:0;
  background:linear-gradient(to top,
    #2B0E13 0%, rgba(43,14,19,.9) 26%,
    rgba(43,14,19,.34) 66%, rgba(43,14,19,.18) 100%);
}
.fbob .ob-w-floor{
  position:absolute; left:0; right:0; bottom:0; top:396px;
  background:linear-gradient(to bottom,
    rgba(43,14,19,0) 0%, #2B0E13 22%, #2B0E13 100%);
}
.fbob .ob-w-text{position:absolute; left:24px; right:24px; top:474px}
.fbob .ob-w-mark{
  margin:0 0 18px;
  display:flex; align-items:center; gap:8px;
  font-size:12px; font-weight:700; letter-spacing:.3em;
  color:var(--on-plate);
}
.fbob .ob-w-mark img{display:block}
.fbob .ob-w-head{
  margin:0;
  font-family:var(--display); font-size:44px; font-weight:500;
  line-height:1; letter-spacing:-.022em;
  color:var(--on-plate); text-wrap:balance;
}
.fbob .ob-w-lede{
  margin:16px 0 0; max-width:31ch;
  font-size:17px; font-weight:400; line-height:1.45;
  color:rgba(244,239,230,.82);
}
.fbob .ob-w-foot{
  position:absolute; left:24px; right:24px;
  bottom:calc(30px + var(--ob-safe));
}
.fbob .ob-w-foot .ob-cta{box-shadow:0 10px 30px rgba(27,95,168,.42)}
.fbob .ob-terms{
  margin:14px 0 0; text-align:center;
  font-size:11px; font-weight:400; line-height:1.5;
  color:rgba(244,239,230,.74);
}
.fbob .ob-terms a{color:inherit; text-decoration:underline}

/* ==========================================================================
   2 · A QUESTION. The mockup's q block: the scroller runs from under the
   chrome at 104 to 98 off the floor, the options are centred in whatever is
   left, and Continue is pinned at bottom:26.
   ========================================================================== */
.fbob .ob-qscroll{
  position:absolute; top:104px; left:0; right:0;
  bottom:calc(72px + 26px + var(--ob-safe));
  display:flex; flex-direction:column;
  padding:0 18px;
}
/* The loader's interruption borrows this layout but has no chrome over it,
   so it starts where the loader's own headline does. */
.fbob .ob-qscroll.is-bare{top:88px}
.fbob .ob-qhead{
  margin:0; flex:0 0 auto;
  font-size:24px; font-weight:600; line-height:1.16;
  letter-spacing:-.012em; text-wrap:balance;
}
.fbob .ob-qhint{
  flex:0 0 auto; margin:8px 0 0;
  font-size:14px; font-weight:500;
  color:var(--dimmer);
}
/* THE OPTIONS GROW INTO SPARE ROOM AND NEVER OUT OF IT.

   `flex:1 0 auto` — grow, DO NOT SHRINK — and the difference from the
   `1 1 auto; min-height:0` this used to be is the whole of the collision the
   owner photographed on /start. Shrinking is what let this box be measured
   smaller than the rows inside it: the head, the hint and the disclosure line
   are all flex:0 0 auto and refuse to give anything up, so on a short window
   every missing pixel came out of this one box, and `justify-content:center`
   then split the overflow evenly ABOVE and below — the first card climbing
   over "Pick two or three. The first three set your feed." and the last one
   landing on the disclosure. Six genre rows on a 700px-tall window is 89px
   of that, 45 of them upwards.

   Refusing to shrink moves the overflow to where something can handle it:
   .ob-qscroll carries .ob-scroll and is already a scroller, so the column
   simply becomes taller than its window and scrolls. Continue is pinned in
   .ob-ctawrap OUTSIDE the scroller and is unaffected either way.

   Nothing changes when there IS room, which is every screen at 390x844 and
   every screen but the genre grid anywhere: with free space to hand out this
   box still grows to fill it and still centres the rows in what it got. That
   is why this is not behind a height query — it is not the viewport that is
   too short, it is the content that is too tall, and a media query cannot
   see how many lines the disclosure sentence wrapped to. */
.fbob .ob-optwrap{
  flex:1 0 auto;
  display:flex; flex-direction:column; justify-content:center;
  padding:8px 0 20px;
}
.fbob .ob-opts{display:flex; flex-direction:column; gap:8px}

.fbob .ob-opt{
  -webkit-appearance:none; appearance:none;
  display:flex; align-items:center; gap:12px;
  width:100%; min-height:60px; padding:11px 14px;
  border-radius:14px;
  background:var(--raise); border:1.5px solid var(--tint-2);
  color:var(--ink);
  font-family:var(--ui); font-size:16px; font-weight:600; line-height:1.25;
  text-align:left; cursor:pointer;
  transition:border-color .16s ease, background-color .16s ease;
}
/* D · the acknowledgement. The tap rebuilds the screen, so the row that
   springs is the NEW row: js/onboard.js carries the tapped key across the
   rebuild and the replacement is born mid-spring. Down, past, settle. */
@keyframes ob-spring{
  0%  {transform:scale(.97)}
  42% {transform:scale(1.02)}
  100%{transform:scale(1)}
}
.fbob .ob-opt.is-tap{animation:ob-spring .34s cubic-bezier(.2,.75,.25,1) both}
.fbob .ob-cover.is-tap{animation:ob-spring .34s cubic-bezier(.2,.75,.25,1) both}
.fbob .ob-opt.is-on{
  background:var(--accent-wash);
  border-color:var(--accent-edge);
}
.fbob .ob-b{flex:1 1 auto; min-width:0; text-align:left}

/* the 46px cover chip on the genre rows */
.fbob .ob-tile{
  flex:0 0 auto; position:relative;
  width:46px; height:46px; border-radius:10px; overflow:hidden;
  background:var(--plate);
}
.fbob .ob-tile img{object-position:center 26%}

/* The tick. A real glyph, the way the mockup draws it: transparent when off
   so the ring is the whole control, filled and dark-on-blue when on. */
.fbob .ob-tick{
  flex:0 0 auto;
  width:24px; height:24px; border-radius:999px;
  display:grid; place-items:center;
  font-size:13px; line-height:1; font-style:normal;
  background:transparent; border:1.5px solid rgba(23,46,91,.2);
  color:transparent;
  transition:background-color .16s ease, border-color .16s ease, color .16s ease;
}
.fbob .ob-tick.is-sq{border-radius:7px}
.fbob .ob-opt.is-on .ob-tick{
  background:var(--coral); border-color:var(--coral); color:var(--on-accent);
}
/* the tick landing. It is a glyph rather than a stroke, so it lands rather
   than draws: the ring pops and the check drops into it. */
@keyframes ob-tickpop{
  0%  {transform:scale(.6)}
  55% {transform:scale(1.12)}
  100%{transform:scale(1)}
}
@keyframes ob-checkin{
  from{opacity:0; transform:scale(.4) rotate(-20deg)}
  to  {opacity:1; transform:none}
}
.fbob .ob-opt.is-tap.is-on .ob-tick{
  animation:ob-tickpop .38s cubic-bezier(.2,.75,.25,1) both;
}
.fbob .ob-cover.is-tap.is-on .ob-mark2{
  animation:ob-tickpop .38s cubic-bezier(.2,.75,.25,1) both;
}

.fbob .ob-ctawrap{
  position:absolute; left:18px; right:18px;
  bottom:calc(26px + var(--ob-safe));
  z-index:15;
}

/* The disclosure line under the genre grid. js/personalize.js calls it the
   sentence that makes the personalization honest rather than a cheque the
   catalogue cannot cash. It is quiet, it is not hidden, and it is inside the
   scroller so it can never be cut off mid-word. */
.fbob .ob-fine{
  flex:0 0 auto;
  /* 16, not the 20 it was, and the four pixels are not an aesthetic judgement.
     This is the LAST thing in the scroller, so its bottom margin is trailing
     dead space above a floor that already holds 98px clear for the button —
     and at 390x844, the size the mockup was drawn at, those four pixels were
     the entire difference between the genre column fitting its window (640 in
     642) and running two pixels past it. Two pixels past it is this sentence's
     descenders sliced off, which is the one thing the note above forbids. */
  margin:0 0 16px;
  font-size:12px; font-weight:400; line-height:1.5;
  color:var(--dimmer);
}

/* the demo card the mockup puts on its five-minutes question */
.fbob .ob-demo{
  flex:0 0 auto; position:relative;
  height:216px; margin:16px 0 0;
  border-radius:18px; overflow:hidden; background:var(--plate);
}
.fbob .ob-demo .ob-plate img{object-position:center 24%}
.fbob .ob-demoscrim{
  background:linear-gradient(to top,
    rgba(19,6,9,.94) 0%, rgba(19,6,9,.5) 52%, rgba(19,6,9,.24) 100%);
}
.fbob .ob-demotitle{
  position:absolute; left:16px; right:16px; bottom:15px; margin:0;
  font-family:var(--display); font-size:24px; font-weight:500;
  line-height:1.1; letter-spacing:-.012em;
  color:var(--on-plate); text-wrap:balance;
  text-shadow:0 2px 16px rgba(0,0,0,.6);
}
.fbob .ob-demotail{
  flex:0 0 auto; margin:14px 0 0;
  font-size:16px; font-weight:400; line-height:1.5;
  color:var(--dim); text-wrap:pretty;
}

/* ==========================================================================
   3 · AN INTERSTITIAL. The mockup's is3 / is6: 430px of painting, a 38px
   serif headline at 462, the body under it, Continue at bottom:30.
   ========================================================================== */
.fbob .ob-i-art{
  position:absolute; top:0; left:0; right:0; height:430px; overflow:hidden;
}
.fbob .ob-i-art .ob-plate img{object-position:center 30%}
.fbob .ob-i-text{position:absolute; left:24px; right:24px; top:462px}
.fbob .ob-i-head{
  margin:0;
  font-family:var(--display); font-size:38px; font-weight:500;
  line-height:1.02; letter-spacing:-.02em;
  text-wrap:balance;
}
.fbob .ob-i-body{
  margin:16px 0 0; max-width:33ch;
  font-size:17px; font-weight:400; line-height:1.5;
  color:var(--dim); text-wrap:pretty;
}
.fbob .ob-i-foot{
  position:absolute; left:24px; right:24px;
  bottom:calc(30px + var(--ob-safe));
}

/* ==========================================================================
   9 · WHICH WOULD YOU CLICK FIRST. The mockup's is8: two up, 3/4.4, hook
   first, and no Continue — the tap IS the answer.
   ========================================================================== */
.fbob .ob-pickscroll{
  position:absolute; top:104px; left:0; right:0;
  bottom:calc(26px + var(--ob-safe));
  padding:0 18px;
}
.fbob .ob-pickpad{height:26px}
.fbob .ob-covers{
  display:grid; grid-template-columns:1fr 1fr; gap:11px;
  margin:18px 0 0;
}
.fbob .ob-cover{
  -webkit-appearance:none; appearance:none;
  position:relative; display:block; width:100%;
  /* 3:4.4 by padding rather than aspect-ratio, which is iOS 15+ and a real
     share of this traffic is older. 4.4/3 = 146.67%. */
  height:0; padding:0 0 146.67%;
  overflow:hidden; border-radius:18px;
  border:1px solid var(--hair);
  background:var(--plate);
  cursor:pointer;
  transition:border-color .16s ease;
}
.fbob .ob-cover.is-on{border:2.5px solid var(--coral)}
.fbob .ob-coverscrim{
  background:linear-gradient(to top,
    rgba(19,6,9,.94) 0%, rgba(19,6,9,.62) 42%, rgba(19,6,9,.16) 100%);
}
.fbob .ob-cap{
  position:absolute; left:13px; right:13px; bottom:13px;
  display:block; text-align:left;
}
.fbob .ob-chead{
  display:block;
  font-family:var(--display); font-size:19px; font-weight:500;
  line-height:1.14; letter-spacing:-.01em;
  color:var(--on-plate);
  text-shadow:0 2px 14px rgba(0,0,0,.6);
}
.fbob .ob-who{
  display:block; margin-top:7px;
  font-size:11px; font-weight:700; letter-spacing:.14em;
  text-transform:uppercase;
  color:rgba(244,239,230,.82);
}
.fbob .ob-mark2{
  position:absolute; right:10px; top:10px;
  width:26px; height:26px; border-radius:999px;
  display:grid; place-items:center;
  font-size:13px; font-weight:700; line-height:1; font-style:normal;
  background:rgba(19,6,9,.5); color:transparent;
}
.fbob .ob-cover.is-on .ob-mark2{background:var(--coral); color:var(--on-accent)}

/* ==========================================================================
   10 · THE LOADER. The mockup's is11: the headline, the picked genres each
   with a tick that lights as the bar passes it, the bar, the percentage, and
   a 236px cover card at top:474 whose title cycles under dot indicators.
   ========================================================================== */
.fbob .ob-l-top{position:absolute; left:22px; right:22px; top:88px}
.fbob .ob-l-head{
  margin:0;
  font-family:var(--display); font-size:32px; font-weight:500;
  line-height:1.06; letter-spacing:-.015em; text-wrap:balance;
}
.fbob .ob-l-sub{
  margin:10px 0 0;
  font-size:15px; font-weight:500; line-height:1.5;
  color:var(--dim);
}
.fbob .ob-picks{display:flex; flex-direction:column; gap:11px; margin:20px 0 0}
.fbob .ob-pick{
  margin:0; display:flex; align-items:center; gap:11px;
  font-size:16px; font-weight:600; line-height:1.3;
}
.fbob .ob-ptick{
  flex:0 0 auto;
  width:22px; height:22px; border-radius:999px;
  display:grid; place-items:center;
  font-size:12px; line-height:1; font-style:normal;
  background:rgba(23,46,91,.1); color:transparent;
  transition:background-color .2s ease, color .2s ease;
}
.fbob .ob-pick.is-on .ob-ptick{background:var(--coral); color:var(--on-accent)}
/* E · a genre ticking off. It pops the once it lights — js/onboard.js
   remembers which rows have already lit, because the rows themselves are
   rebuilt three times a second and a class alone would pop them every time. */
@keyframes ob-labelslide{
  from{opacity:.55; transform:translate3d(6px,0,0)}
  to  {opacity:1;   transform:none}
}
.fbob .ob-pick.is-pop .ob-ptick{
  animation:ob-tickpop .38s cubic-bezier(.2,.75,.25,1) both;
}
.fbob .ob-pick.is-pop .ob-b{
  animation:ob-labelslide .38s cubic-bezier(.2,.75,.25,1) both;
}

.fbob .ob-loadtrack{
  height:5px; margin:26px 0 0;
  border-radius:999px; background:var(--hair); overflow:hidden;
}
/* Same reason as the phase bars, and the same trick: the element is new on
   every paint, so js/onboard.js starts it at the last percentage and lets it
   run to this one. Before this the bar jumped its seven per cent. */
.fbob .ob-loadfill{
  display:block; height:100%; width:100%;
  transform:scaleX(0); transform-origin:0 50%;
  border-radius:999px; background:var(--coral);
}
.fbob .ob-pct{
  margin:10px 0 0;
  font-size:13px; font-weight:600; color:var(--dimmer);
  font-variant-numeric:tabular-nums;
}

.fbob .ob-l-cover{position:absolute; left:22px; right:22px; top:474px}
.fbob .ob-covercard{
  position:relative; height:236px;
  border-radius:20px; overflow:hidden; background:var(--plate);
}
.fbob .ob-covercard .ob-plate img{object-position:center 26%}
.fbob .ob-cardscrim{
  background:linear-gradient(to top,
    rgba(19,6,9,.92) 0%, rgba(19,6,9,.42) 54%, rgba(19,6,9,.14) 100%);
}
.fbob .ob-covertitle{
  position:absolute; left:16px; right:16px; bottom:14px; margin:0;
  font-family:var(--display); font-size:21px; font-weight:500; line-height:1.14;
  color:var(--on-plate); text-shadow:0 2px 14px rgba(0,0,0,.6);
}
.fbob .ob-dots{display:flex; justify-content:center; gap:6px; margin:14px 0 0}
.fbob .ob-dot{
  display:block; width:6px; height:6px; border-radius:999px;
  background:var(--tint-3);
  transition:width .2s ease, background-color .2s ease;
}
.fbob .ob-dot.is-on{width:18px; background:var(--coral)}

/* ---- G · the ghost word -------------------------------------------------
   Newsreader at 132px and five per cent of the ink, drifting through the
   empty band the mockup leaves between the percentage and the cover card and
   passing BEHIND the card — it is first in the DOM, so the card paints over
   it without either of them needing a z-index. It is the wordmark, so it is
   no new copy, and it is aria-hidden, so it is no new sentence either. */
.fbob .ob-ghostword{
  position:absolute; left:0; right:0; top:330px; height:210px;
  z-index:0; pointer-events:none; overflow:hidden;
  display:flex; align-items:center; justify-content:center;
}
.fbob .ob-ghostword span{
  display:block; white-space:nowrap;
  font-family:var(--display); font-size:66px; font-weight:500;
  letter-spacing:.02em; line-height:1;
  color:var(--ink); opacity:.06;
  animation:ob-drift 26s ease-in-out infinite both;
}
@keyframes ob-drift{
  0%  {transform:translate3d(-5%,5px,0) scale(1)}
  50% {transform:translate3d(5%,-7px,0) scale(1.035)}
  100%{transform:translate3d(-5%,5px,0) scale(1)}
}

.fbob .ob-l-foot{
  position:absolute; left:22px; right:22px;
  bottom:calc(26px + var(--ob-safe));
}

/* ==========================================================================
   11 · THE PAYOFF. The mockup's is12: three full-bleed 186px art cards, each
   with a 22px serif headline over a gradient and a meta line, and a floor
   that fades the ground up behind Start reading.
   ========================================================================== */
.fbob .ob-res{position:absolute; top:0; right:0; bottom:0; left:0}
.fbob .ob-resin{padding:74px 22px 130px}
.fbob .ob-r-head{
  margin:0;
  font-family:var(--display); font-size:34px; font-weight:500;
  line-height:1.04; letter-spacing:-.016em; text-wrap:balance;
}
.fbob .ob-r-note{
  margin:10px 0 0;
  font-size:15px; font-weight:500; color:var(--dim);
}
.fbob .ob-cards{
  list-style:none; margin:22px 0 0; padding:0;
  display:flex; flex-direction:column; gap:13px;
}
.fbob .ob-card{
  position:relative; height:186px;
  border-radius:18px; overflow:hidden; background:var(--plate);
}
.fbob .ob-card .ob-plate img{object-position:center 24%}
.fbob .ob-cardscrim2{
  background:linear-gradient(to top,
    rgba(19,6,9,.94) 0%, rgba(19,6,9,.5) 52%, rgba(19,6,9,.18) 100%);
}
.fbob .ob-cardhead{
  position:absolute; left:16px; right:16px; bottom:36px; margin:0;
  font-family:var(--display); font-size:22px; font-weight:500;
  line-height:1.12; letter-spacing:-.01em;
  color:var(--on-plate); text-wrap:balance;
  text-shadow:0 2px 14px rgba(0,0,0,.6);
}
.fbob .ob-meta{
  position:absolute; left:16px; right:16px; bottom:14px; margin:0;
  font-size:12px; font-weight:600; color:rgba(244,239,230,.82);
  font-variant-numeric:tabular-nums;
}
/* The disclosure again, and for the same reason as on the genre grid. It
   sits in the scroller above the floor, so the whole sentence is reachable. */
.fbob .ob-resin .ob-fine{margin:18px 0 0; max-width:none}

.fbob .ob-r-foot{
  position:absolute; left:0; right:0; bottom:0; z-index:15;
  padding:16px 22px calc(26px + var(--ob-safe));
  background:linear-gradient(to top,
    var(--ground) 0%, var(--ground) 62%, rgba(231,224,211,0) 100%);
}
.fbob .ob-r-foot .ob-cta{box-shadow:0 10px 26px rgba(27,95,168,.3)}

/* ==========================================================================
   The loader's two interruptions. Not in the mockup — they are this engine's
   own, and they are the reason the loader is not dead time. They borrow the
   mockup's demo-question layout exactly rather than inventing a third look.
   ========================================================================== */
.fbob .ob-yn{display:grid; grid-template-columns:1fr 1fr; gap:10px}
/* Two buttons that are the answer, not two rows with a state: there is no
   selected yes/no to show, so the ring that would sit there is not drawn. */
.fbob .ob-yn .ob-tick{display:none}
.fbob .ob-yn .ob-opt{justify-content:center; text-align:center; min-height:56px}
.fbob .ob-yn .ob-b{flex:0 0 auto; text-align:center}

/* ---- focus, and the one ring ------------------------------------------- */
.fbob button:focus-visible,
.fbob a:focus-visible{
  outline:2px solid var(--ring);
  outline-offset:2px;
  border-radius:12px;
}

/* ==========================================================================
   SHORTER PHONES. The mockup is drawn at 844. A 667 or 736 phone has to give
   something up, and it is never the headline or the button: the paintings
   come down and the blocks that sit on absolute offsets ride up with them.
   ========================================================================== */
@media (max-height:800px){
  .fbob .ob-w-art{height:540px}
  .fbob .ob-w-floor{top:350px}
  .fbob .ob-w-text{top:400px}
  .fbob .ob-w-head{font-size:38px}
  .fbob .ob-i-art{height:380px}
  .fbob .ob-i-text{top:410px}
  .fbob .ob-i-head{font-size:34px}
  .fbob .ob-l-top{top:70px}
  .fbob .ob-ghostword{top:296px; height:190px}
  .fbob .ob-ghostword span{font-size:58px}
  .fbob .ob-l-cover{top:436px}
  .fbob .ob-covercard{height:200px}
  .fbob .ob-resin{padding-top:56px}
  /* And the paintings on the GENRE ROWS come down with them, which is the one
     screen the two blocks below had been missing. `min-height:54px` never
     touched a genre row: the row is a border box around a 46px cover chip
     plus 11px of padding, so the chip — not the min-height — is what makes it
     70 tall, and six of those is 460px of a window that may only have 498.
     The chip is an identifier, not an illustration, and it is still one at 38. */
  .fbob .ob-opt{padding:9px 14px}
  .fbob .ob-tile{width:38px; height:38px; border-radius:9px}
}
@media (max-height:700px){
  .fbob .ob-w-art{height:460px}
  .fbob .ob-w-floor{top:290px}
  .fbob .ob-w-text{top:330px}
  .fbob .ob-w-head{font-size:32px}
  .fbob .ob-w-lede{font-size:15px; margin-top:12px}
  .fbob .ob-i-art{height:310px}
  .fbob .ob-i-text{top:336px}
  .fbob .ob-i-head{font-size:29px}
  .fbob .ob-i-body{font-size:15px}
  .fbob .ob-l-top{top:52px}
  .fbob .ob-ghostword{top:246px; height:160px}
  .fbob .ob-ghostword span{font-size:50px}
  .fbob .ob-l-head{font-size:26px}
  .fbob .ob-l-cover{top:372px}
  .fbob .ob-covercard{height:168px}
  .fbob .ob-covertitle{font-size:18px}
  .fbob .ob-opt{min-height:54px}
  .fbob .ob-qhead{font-size:21px}
  .fbob .ob-tile{width:34px; height:34px}
}

/* The question column is NOT ridden up by a height query the way the painting
   screens above are, and that is deliberate: see .ob-optwrap. Its blocks are
   text whose height depends on where it wrapped, not art on a known offset,
   so it is given a scroller and allowed to use it rather than a table of
   guesses about how tall six rows and a disclosure sentence came out. */

/* ==========================================================================
   REDUCED MOTION. Not "the same thing, faster" — OFF.

   One rule, and it is deliberately the blunt one: every animation and every
   transition inside the funnel, including any this file gains later and any
   js/onboard.js sets inline. The keyframes above all END at the resting
   state, and a cancelled animation applies none of its frames, so switching
   them off leaves every element exactly where it is supposed to finish:
   opacity 1, no transform, no blur. Nothing here needs a second declaration
   to un-hide it, and that is the property that makes this safe.

   js/onboard.js reads the same query and does not START the plate morph, the
   word split's animation classes, or either bar's two-frame release — an
   inline transform mid-transition would otherwise be left standing when the
   transition it was waiting on never runs.
   ========================================================================== */
@media (prefers-reduced-motion:reduce){
  .fbob *,
  .fbob *::before,
  .fbob *::after{
    animation:none !important;
    transition:none !important;
  }
  .fbob .ob-w{filter:none; opacity:1; transform:none}
}
