/* ==========================================================================
   Factbox — the end of a story, and the wall.

   Additive only. It reuses .pane, .go, .ghost and .fine from app.css and
   paints on top; it redefines none of them. Every colour, radius and tap size
   is a token — this sheet had its own copies of the palette once, so a change
   to --dimmer moved every other surface and not this one. NOTHING IN THIS FILE
   NAMES A COLOUR. The reader's palette is the one `:root` block in
   css/reader-rail.css, which read.html loads after this file.

   TWO SURFACES, AND EACH IS REALLY TWO
   ------------------------------------
   .pane.rec       the end of a story, played in beats:
                     .ec-p1  what the reader just did, over the plate they
                             finished on. About a second.
                     .ec-p2  the next episode, and the countdown that opens
                             it. js/recommend.js drives which is on screen by
                             putting .is-p1 / .is-p2 on the pane; when neither
                             is set the reader has not arrived yet and the
                             pane is the plate alone.
   .fbg            the wall, and it is no longer a pane. A locked story runs
                   for real and stops at a boundary; this is what the gesture
                   past that boundary opens, over the story rather than
                   instead of it:
                     .fbg-auth  "Keep reading."  an account, so the progress
                                is theirs and so a payment can be.
                     .fbg-buy   the amount, the terms, one button.
                   .is-auth / .is-buy on .fbg say which. BOTH ARE ALWAYS IN
                   THE DOM: the hidden one is visibility:hidden, which takes
                   it out of the tab order and out of the accessibility tree
                   while leaving its words where a checker can read them.
                   display:none would do the first two and is animatable by
                   nothing, so a crossfade would have to be a second mechanism.
   .pane.paywall   the fallback, for a page whose account.js or auth never
                   arrived: one screen, two sentences and a way in.

   THE ONE LAYOUT RULE THEY ALL OBEY
   ---------------------------------
   THE BUTTON IS NEVER BELOW --bottom-safe, AND NEVER BELOW THE FOLD.

   --bottom-safe in app.css is max(13vh, inset + 60px toolbar + 4px): the line
   the Instagram and TikTok in-app browsers draw their own toolbar over, and
   that is essentially all of this traffic. A control under it cannot be
   tapped. So every action group is absolutely positioned against the BOTTOM
   rather than at a percentage of the height —

       bottom: calc(var(--bottom-safe) + 8px)

   which clears the toolbar at every viewport height rather than at the one
   the last person tested on. The copy above reserves that much room, and when
   it is taller than what is left — a phone turned sideways — it scrolls
   behind a docked footer rather than pushing the button through the floor.
   ========================================================================== */

/* ==========================================================================
   Shared: the pane, its plate, and the action group pinned to its floor.
   ========================================================================== */

.pane.rec{
  position:relative;
  /* The plate is full-bleed; the words inside it are not. app.css pads the
     pane itself, so the padding moves inward one level. */
  padding-left:0; padding-right:0;
  justify-content:center;
  justify-content:safe center;
  overflow:hidden;
  text-align:left;
}

/* The fallback pane, for a page whose recommend.js built the wall and then
   could not — no plate, no sheet, four elements and a way out. It keeps
   app.css's own .pane padding, because the horizontal zero above exists for
   a full-bleed plate and there is no plate on this one; without that the
   sentence runs into both edges of the phone. */
.pane.paywall{
  position:relative;
  justify-content:center;
  justify-content:safe center;
}

/* A full-bleed plate with the reader's own scrim over it. Same element in
   four places, so it is one rule: .ec-plate behind the completion moment,
   .ec-heroart behind the next story. The wall has no plate of its own any
   more: the story's own card is behind it, still on screen. */
.ec-plate,
.ec-heroart{
  position:absolute; top:0; right:0; bottom:0; left:0;
  overflow:hidden;
  background:var(--night);
}
.ec-plate img,
.ec-heroart img{
  width:100%; height:100%;
  object-fit:cover; object-position:center 32%;
  display:block;
}
.sc-scrim{
  position:absolute; top:0; right:0; bottom:0; left:0;
  display:block;
  background:var(--pane-scrim);
}
.ec-heroart .sc-scrim{background:var(--band-scrim)}

/* The action group. Absolute, so the copy above cannot push it down; bottom
   anchored, so it cannot fall under the in-app toolbar.

   left/right + auto margins is what centres an absolutely positioned box; a
   `margin:0 auto` alone would not, and `inset` is Safari 14.1+ and a share of
   this traffic is older than that. */
.pane.rec .ec-act{
  position:absolute;
  left:22px; right:22px;
  bottom:calc(var(--bottom-safe) + 8px);
  width:auto; max-width:520px;
  margin-left:auto; margin-right:auto;
  z-index:3;
  display:flex; flex-direction:column; align-items:stretch;
  text-align:center;
}

/* The two groups whose screen SCROLLS get a ground of their own.

   .ec-p2 scrolls when the copy is taller than the viewport, and it scrolls
   BEHIND the group — so without this a line of text passes through
   the gap between the button and the link on its way past. The ground has to
   fade, or the same line is cut in half by a hard edge; --ground-0 is the
   transparent end of --ground, because `transparent` is rgba(0,0,0,0) and
   fades through grey on iOS. Both screens paint --ground, so at rest it is
   invisible.

   The wall's own action group is no longer in this list because the wall is
   no longer a pane: it is .fbg, a sheet with its own opaque ground. */
.pane.rec .ec-act{
  padding-top:22px;
  background:linear-gradient(to top,
    var(--ground) 0%, var(--ground) 64%, var(--ground-0) 100%);
}

/* One class for the crossfade, because all four screens do the same thing:
   fade and lift into place, and go away without leaving a tab stop behind. */
.ec-p1,
.ec-p2{
  position:absolute; top:0; right:0; bottom:0; left:0;
  opacity:0; visibility:hidden; pointer-events:none;
  transform:translateY(14px);
  transition:opacity .45s ease, transform .45s cubic-bezier(.2,.75,.25,1),
             visibility 0s linear .45s;
}
.pane.rec.is-p1 .ec-p1,
.pane.rec.is-p2 .ec-p2{
  opacity:1; visibility:visible; pointer-events:auto;
  transform:none;
  transition:opacity .45s ease, transform .45s cubic-bezier(.2,.75,.25,1),
             visibility 0s linear 0s;
}

/* An action group belongs to the SCREEN it acts on, and lives on the PANE.

   On the pane, because .ec-p2 scrolls when the copy is taller than the
   viewport, and an absolutely positioned child of a scrolling
   box scrolls away with its content — which is the bottom anchor above
   defeated on exactly the short phones it exists for. And .ec-act has a
   second reason: tools/compose.py's sign-up block appends its own line beside
   it, on the card.

   To the screen, because the completion moment is a moment, and a button on
   it is something to do instead of noticing. */
.pane.rec .ec-act,
.pane.rec .ec-signin{
  opacity:0; visibility:hidden; pointer-events:none;
  transition:opacity .45s ease, visibility 0s linear .45s;
}
.pane.rec.is-p2 .ec-act,
.pane.rec.is-p2 .ec-signin{
  opacity:1; visibility:visible; pointer-events:auto;
  transition:opacity .45s ease, visibility 0s linear 0s;
}

/* The screen-reader announcement that goes with the countdown. Off screen,
   never display:none — a display:none live region is announced by nothing. */
.pane.rec .ec-act .ec-say{
  position:absolute; width:1px; height:1px;
  margin:-1px; padding:0; border:0;
  overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%);
  white-space:nowrap;
  /* Out-specifies the rule above that hides the action group in beat one.
     A live region inside a visibility:hidden box is not in the accessibility
     tree, so the one announcement the countdown makes would be made to
     nobody — and it is written in the same frame the group becomes visible,
     which is exactly the frame a screen reader may not have caught up with. */
  visibility:visible;
}

/* ==========================================================================
   1. THE END OF A STORY

   Beat one: a tick, the subject, and how much of it is finished. Centred over
   the plate the reader stopped on, which they were looking at a swipe ago and
   which is therefore already decoded — this screen starts no download.
   ========================================================================== */

.ec-p1{
  display:flex; flex-direction:column;
  align-items:center; justify-content:center;
  text-align:center;
  padding:var(--top-safe) 30px var(--bottom-safe);
  z-index:2;
}
.ec-badge{
  flex:0 0 auto;
  width:44px; height:44px; border-radius:var(--pill);
  display:flex; align-items:center; justify-content:center;
  background:var(--coral); color:var(--on-accent);
  box-shadow:var(--shadow-go);
}
.ec-badge svg{width:20px; height:20px; display:block}

.pane.rec .ec-eyebrow{
  margin:16px 0 0; color:var(--dimmer);
  font-size:var(--fs-micro); font-weight:var(--w-label);
  letter-spacing:var(--ls-caps); text-transform:uppercase;
}
/* The subject. Display serif, because it is the story's name and not a label. */
.pane.rec .ec-line1{
  margin:10px 0 0; max-width:20ch; color:var(--parchment);
  font-family:var(--display); font-weight:var(--w-display);
  font-size:1.5625rem; line-height:1.2; letter-spacing:var(--ls-title);
  text-wrap:balance;
}
/* "3 of 8 complete" — the reader's own record when it is theirs to show, and
   the one completion we watched when it is not. js/recommend.js decides that;
   this file only sets the type. */
.pane.rec .ec-line2{
  margin:6px 0 0; color:var(--accent-ink);
  font-size:var(--fs-micro); font-weight:var(--w-label);
  letter-spacing:var(--ls-caps); text-transform:uppercase;
  font-variant-numeric:tabular-nums;
}

/* ---- beat two: the next episode -----------------------------------------
   The cover is the argument, so it is the screen rather than a 60px row: a
   plate down to the fold, its name and length written into the foot of it,
   and the subject's dots underneath. */
.ec-p2{
  display:flex; flex-direction:column; justify-content:flex-start;
  background:var(--ground);
  /* When the copy is taller than what the action group leaves — a phone
     turned sideways — the screen scrolls behind it rather than pushing the
     button through the floor. */
  overflow-y:auto; overscroll-behavior:contain;
  padding-bottom:calc(var(--bottom-safe) + 186px);
}
.ec-hero{
  position:relative;
  flex:0 0 auto;
  height:min(512px, 58vh);
  overflow:hidden;
}
.ec-herotext{
  position:absolute; left:20px; right:20px; bottom:14px;
  z-index:2; text-align:left;
}
/* The words get their OWN scrim, and this is not decoration.

   The block is four lines tall — an eyebrow, a position, a three-line
   headline and a runtime — so its top edge lands around 39% up the plate,
   where the band gradient has already lifted to about .9. Measured over the
   palest cover in the season that put "Up next" at 1.99:1 in --accent-ink,
   which is a label nobody can read.

   A second scrim, local to the block, composites with the band to about .978
   and takes the same label to 7.7:1 — while leaving the top two thirds of the
   painting exactly as bright as it was. Deepening the band instead would have
   cost the plate. z-index:-1 puts it behind the words and, because this
   element is its own stacking context, still in front of the plate. */
.ec-herotext::before{
  content:"";
  position:absolute; left:-20px; right:-20px; top:-26px; bottom:-14px;
  z-index:-1;
  background:var(--pane-scrim);
  pointer-events:none;
}
.ec-nonext{
  position:static; margin-top:auto; margin-bottom:auto;
  padding:0 22px;
}
.pane.rec .ec-up{
  margin:0 0 8px; color:var(--accent-ink);
  font-size:var(--fs-micro); font-weight:var(--w-label);
  letter-spacing:var(--ls-caps); text-transform:uppercase;
}
.pane.rec .ec-of{
  margin:0 0 10px; color:var(--dimmer);
  font-size:var(--fs-micro); font-weight:var(--w-label);
  letter-spacing:var(--ls-caps); text-transform:uppercase;
  font-variant-numeric:tabular-nums;
}
.pane.rec .ec-head{
  margin:0; color:var(--parchment);
  font-family:var(--display); font-weight:var(--w-display);
  font-size:2.25rem; line-height:1.08; letter-spacing:-.014em;
  text-wrap:balance;
  /* RETUNED FOR A TITLE, NOT A HOOK. js/recommend.js draws the story's TITLE
     here now — see promise() and the note above it — and the titles in this
     season measure 9 to 46 characters, mean 27, against hooks that ran to a
     whole paragraph. Three clamped lines at 2rem was a box sized for the old
     value and left a visible gap under nearly every title.

     So: one step up in size, because a 27-character line has the room for it
     and this is the headline of the screen; and TWO lines rather than three,
     which is what the longest title in the season needs at this size. The
     clamp stays because the fallback is still a hook — a title-less row would
     otherwise set a paragraph in display serif at 2.25rem. */
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
  overflow:hidden;
}
.pane.rec .ec-mins{
  margin:12px 0 0; color:var(--dimmer);
  font-size:var(--fs-meta); font-weight:var(--w-label);
  font-variant-numeric:tabular-nums;
}

/* One dot per story in the subject: filled for finished, ringed for the one
   about to open, faint for the rest. aria-hidden in the markup — the two
   lines above say the same thing in words. */
.ec-dots{
  display:flex; align-items:center; gap:7px;
  padding:16px 22px 0;
}
.ec-dots i{
  display:block; width:7px; height:7px; border-radius:var(--pill);
  background:var(--tint-3);
}
.ec-dots i.is-on{background:var(--coral)}
.ec-dots i.is-next{box-shadow:inset 0 0 0 1.5px var(--coral)}

/* ---- the control, and the clock ------------------------------------------
   The remaining seconds are TEXT. There is no shrinking bar and no sweeping
   ring: a reader who has asked for reduced motion still gets the advance, and
   a cue they cannot see is not a cue. */
.pane.rec .ec-timer{
  margin:0 0 10px; text-align:center; color:var(--dimmer);
  font-size:var(--fs-meta); font-weight:var(--w-label);
  font-variant-numeric:tabular-nums;
  min-height:1.4em;                    /* no reflow between "3…" and "Opening…" */
}
.ec-go{
  width:100%; max-width:520px; margin:0 auto;
  text-align:center; text-decoration:none; line-height:1.2;
}
a.ec-go{
  /* .go is written for <button>; an <a> needs the box spelled out. */
  display:flex; align-items:center; justify-content:center;
}
/* "Back to Explore" — the cancel. An ordinary link, so it opens in a new tab,
   focuses with the keyboard and reads as what it is. */
.pane.rec .ec-out{
  display:flex; align-items:center; justify-content:center;
  width:auto; margin:2px auto 0;
  color:var(--dimmer); text-decoration:none;
  font-size:var(--fs-sub); font-weight:var(--w-strong);
  padding:10px 12px; min-height:var(--tap);
}
/* The offer. One line, and never drawn for a reader who already pays —
   js/recommend.js does not build it for them, so there is no rule here that
   could be beaten by a cascade. */
.pane.rec .ec-offer{margin:6px auto 0}

/* Short viewports: 667px minus a webview toolbar is not much, and a phone
   turned sideways is under 400. The cover is the only thing that can give
   ground without losing a word. */
@media (max-height:760px){
  .ec-hero{height:min(420px, 52vh)}
  .pane.rec .ec-head{font-size:1.75rem; -webkit-line-clamp:2}
}
@media (max-height:640px){
  .ec-hero{height:min(320px, 46vh)}
  .ec-badge{width:36px; height:36px}
  .ec-badge svg{width:17px; height:17px}
  .pane.rec .ec-line1{font-size:1.375rem}
  .pane.rec .ec-mins{margin-top:8px}
  .ec-dots{padding-top:10px}
}
/* A phone turned sideways is not a short screen, it is a WIDE one: 375px of
   height, of which the in-app toolbar and the action group take about 190.
   The words stay written into the foot of the plate — moving them out of it
   would mean two copies of the same block in the markup — and everything
   above gives ground until the pair fits. Measured at 667x375 with the
   toolbar: the dots clear the action group by 14px. */
@media (max-height:560px){
  .ec-hero{height:min(214px, 50vh)}
  .ec-p2{padding-bottom:calc(var(--bottom-safe) + 140px)}
  .pane.rec .ec-head{font-size:1.375rem; -webkit-line-clamp:2}
  .pane.rec .ec-of{margin-bottom:6px}
  .pane.rec .ec-mins{margin-top:6px}
  .pane.rec .ec-line1{font-size:1.25rem}
  .pane.rec .ec-eyebrow{margin-top:10px}
  .pane.rec .ec-timer{margin-bottom:4px}
  .ec-dots{padding-top:10px}
}

/* ==========================================================================
   1b. /firststory only — the sign-up ask under the button.

   `.has-ask` is set by the block tools/compose.py appends, so the end card
   every other story ends on is untouched by everything in this section. The
   ask replaces the button and appends its own sign-in line to the CARD, not
   to the action group, so the two are positioned as one stack here: the
   sign-in line takes the floor and the action group sits one tap above it.

   The offer line is hidden there on purpose. /firststory's whole button IS
   the offer; two of them, one above the other, is the same ask twice. There
   is no countdown on that page either — js/recommend.js will not start one
   when the caller passes its own label, because walking a reader into the
   next story while asking them to sign up is two screens arguing.
   ========================================================================== */
.pane.rec.has-ask .ec-offer{display:none}
.pane.rec.has-ask .ec-out{display:none}
.pane.rec.has-ask .ec-act{
  bottom:calc(var(--bottom-safe) + 8px + var(--tap) + 6px);
}
.pane.rec .ec-signin{
  position:absolute;
  left:22px; right:22px;
  bottom:calc(var(--bottom-safe) + 8px);
  width:auto; max-width:520px;
  margin-left:auto; margin-right:auto;
  z-index:3;
  /* flex, not a plain line box: an inline-block with a 44px tap height inside
     a <p> sits on the text baseline and drags the block taller than 44px,
     which is what would push it through the floor. */
  display:flex; flex-wrap:wrap; align-items:center; justify-content:center;
  column-gap:5px;
  text-align:center;
}
.pane.rec .ec-signin a{
  color:var(--accent-ink); font-weight:var(--w-strong); text-decoration:none;
  display:inline-block; padding:8px 6px; min-height:var(--tap); line-height:28px;
}

/* ==========================================================================
   2. THE WALL — a sheet OVER the story, not a screen instead of it.

   WHAT THIS REPLACED. Two full-screen panes: a manufactured "interruption"
   carrying the story's own first card, and an offer screen behind a button.
   The reader met neither the story nor the offer — they met a reproduction
   of the story, and then a price.

   Now a locked story really opens and really runs. read.html draws its first
   cards on their own plates, with their own credit lines and the same
   progress rail every other story has, and the run stops at a boundary the
   reader can see: the next card's opening line, dimmed, under a hairline and
   a padlock. Trying to keep reading is what opens the sheet.

   THREE SHEETS, ONE HOST, AND ALL THREE ARE ALWAYS IN THE DOM.
     .fbg-quiz  the eleven questions   js/onboard.js, mounted into this box
     .fbg-auth  "Keep reading."   an account, so the progress is theirs
     .fbg-buy   "Finish …"        the amount, the terms, one button

   .is-quiz / .is-auth / .is-buy on .fbg say which. The hidden ones are
   visibility:hidden, which takes them out of the tab order and out of the
   accessibility tree while leaving their words where a checker reading the
   page can find them. display:none would do the first two and is animatable
   by nothing, so the crossfade would have to be a second mechanism.

   THE SHEETS ARE POSITIONED, NOT FLOWED. Siblings in a flex column would
   reserve space for the hidden ones and push the visible one up the screen by
   their height. All three are absolutely positioned against the host's floor,
   so the one on screen is the only one that has a place on it.

   .fbg-quiz CARRIES NO LOOK OF ITS OWN. Everything inside it is css/onboard.css,
   which is scoped entirely under .fbob and was written against this box —
   520px, 22px of padding, 94vh with its own overflow. The only thing added
   below is the two lines that let it be the sheet on screen, and the padding
   trim that keeps the engine's own 44px top row from sitting under a second
   one. Styling its contents from here would be two files with an opinion
   about one element.

   THE FLOOR IS --bottom-safe. The Instagram and TikTok in-app browsers draw
   their own toolbar over the bottom of the viewport and that is essentially
   all of this traffic, so every sheet's padding ends on that line rather than
   on the viewport's. A control under it cannot be tapped.

   EVERY FIGURE ON THESE SHEETS IS READ OUT OF js/account.js. There is nothing
   in this file that could carry one — no ::before content, no counter, no
   attr() — and that is deliberate: tools/check-regressions.js greps this
   stylesheet for a typed price along with the two files that build the
   screen.
   ========================================================================== */

.fbg{
  position:fixed; top:0; right:0; bottom:0; left:0;
  z-index:60;
  pointer-events:none;
}

/* The story stays visible under it. Dimmed, because the sheet is the thing
   being read now; not hidden, because the sentence the reader stopped on is
   the whole argument. */
.fbg-veil{
  position:absolute; top:0; right:0; bottom:0; left:0;
  background:var(--glass-deep);
  opacity:0; visibility:hidden; pointer-events:none;
  transition:opacity .3s ease, visibility 0s linear .3s;
}
.fbg.is-quiz .fbg-veil,
.fbg.is-auth .fbg-veil,
.fbg.is-buy .fbg-veil{
  opacity:1; visibility:visible; pointer-events:auto;
  transition:opacity .3s ease, visibility 0s linear 0s;
}

.fbg-sheet{
  position:absolute; left:0; right:0; bottom:0;
  z-index:2;
  width:auto; max-width:520px; margin-left:auto; margin-right:auto;
  padding:22px 22px calc(var(--bottom-safe) + 10px);
  border-radius:24px 24px 0 0;
  background:var(--menu);
  box-shadow:var(--shadow-2), inset 0 0 0 1px var(--hair);
  text-align:left;
  max-height:94vh; overflow-y:auto; overscroll-behavior:contain;
  opacity:0; visibility:hidden; pointer-events:none;
  transform:translateY(18px);
  transition:opacity .32s ease, transform .32s cubic-bezier(.2,.75,.25,1),
             visibility 0s linear .32s;
}
.fbg.is-quiz .fbg-quiz,
.fbg.is-auth .fbg-auth,
.fbg.is-buy .fbg-buy{
  opacity:1; visibility:visible; pointer-events:auto;
  transform:none;
  transition:opacity .32s ease, transform .32s cubic-bezier(.2,.75,.25,1),
             visibility 0s linear 0s;
}

/* The quiz is the tallest of the three and the only one that scrolls inside
   itself. .fbob is a flex column with its own max-height and .ob-body is what
   scrolls, so the sheet must NOT be a second scroller around it — two nested
   overflows on a phone is the fight where the inner one loses and the footer
   button walks off the bottom. Top padding comes down because the engine
   draws its own 44px top row.

   It is focusable (tabindex="-1", set in js/recommend.js) so the dialog can
   take focus without the pane itself joining the tab order; nothing may draw
   a focus ring on the box for that. */
.fbg .fbg-quiz{
  padding-top:14px;
  overflow-y:visible;
}
.fbg .fbg-quiz:focus{outline:none}

/* --------------------------------------------------------------------------
   THE QUIZ IS FULL SCREEN. The other two sheets are not.

   .fbg-sheet is a bottom sheet on purpose: an auth form and a price are
   interruptions, and the sentence the reader stopped on is the argument for
   answering them, so the story stays visible and dimmed behind.

   Onboarding is not an interruption. It is eleven screens and about a minute
   of somebody's attention, and it is the mockup's own shape — full phone
   frames, not a card over a story. Half a painting sliding around behind a
   questionnaire reads as an advert that would not close; it also costs the
   quiz the vertical room it needs, which is how the loader ended up with the
   Back button, a headline, a progress bar and a disabled button competing for
   the bottom third of a 390x844 phone.

   So: same host, same state machine, same teardown protection. Only the box
   changes shape. Written as `.fbg.is-quiz .fbg-sheet.fbg-quiz` — TWO classes
   on the subject, not one — because .fbg-sheet's own bottom/max-height/radius
   are set at (0,1,0) and a single class here would tie, and a tie is decided
   by source order, which is not a thing to depend on across an edit.
   -------------------------------------------------------------------------- */
.fbg.is-quiz .fbg-sheet.fbg-quiz{
  top:0;
  max-width:none;
  max-height:none;
  padding:14px 20px calc(var(--bottom-safe) + 10px);
  border-radius:0;
  box-shadow:none;
  transform:none;              /* it does not slide up; it IS the screen */
  overflow-y:auto;             /* full height, so the inner scroller is gone */
  overscroll-behavior:contain;
  /* A column, so the engine's own column can be told to fill it. Without
     this the quiz keeps its sheet-sized height and sits in the top half of
     the phone with the Continue button floating in the middle of nowhere. */
  display:flex; flex-direction:column;
}
/* Nothing to dim when nothing shows through. Keeping the veil painted would
   also darken the quiz itself on the frame where visibility flips. */
.fbg.is-quiz .fbg-veil{
  background:var(--menu);
}
@media (prefers-reduced-motion: reduce){
  .fbg.is-quiz .fbg-sheet.fbg-quiz{transition:none}
}

/* ---- the words ----------------------------------------------------------- */
.fbg .fbg-eyebrow{
  margin:0 0 10px;
  font-size:var(--fs-micro); font-weight:var(--w-label);
  letter-spacing:var(--ls-caps); text-transform:uppercase;
  color:var(--dimmer);
  font-variant-numeric:tabular-nums;
}
.fbg .fbg-head{
  margin:0; color:var(--parchment);
  font-family:var(--display); font-weight:var(--w-display);
  font-size:1.875rem; line-height:1.1; letter-spacing:var(--ls-head);
  text-wrap:balance;
}
.fbg .fbg-headbig{font-size:2.25rem}
.fbg .fbg-sub{
  margin:8px 0 0; max-width:34ch;
  color:var(--dim);
  font-size:var(--fs-body); font-weight:var(--w-body); line-height:1.5;
  text-wrap:pretty;
}

/* ---- the account sheet's three doors -------------------------------------
   One shape, three grounds: the provider that is configured gets the solid
   button, everything else a hairline. A door that is not there is not drawn
   greyed out — see APPLE_ON in js/recommend.js. */
.fbg .fbg-btn{
  display:flex; align-items:center; justify-content:center; gap:11px;
  width:100%; margin-top:8px;
  padding:11px 18px; min-height:46px;
  border:0; border-radius:var(--pill); cursor:pointer;
  font-family:var(--ui); font-size:var(--fs-sub); font-weight:var(--w-strong);
  background:var(--parchment); color:var(--menu);
  line-height:1.2;
}
.fbg .fbg-google{margin-top:18px}
.fbg .fbg-line{
  background:none; color:var(--parchment);
  box-shadow:inset 0 0 0 1px var(--tint-3);
}
.fbg .fbg-btn[disabled]{opacity:.6; cursor:default}
.fbg-mark{flex:0 0 20px; width:20px; height:20px; display:block}
.fbg-mark svg{width:100%; height:100%; display:block}

.fbg .fbg-alt{
  display:flex; align-items:center; justify-content:center;
  margin:6px 0 0; min-height:var(--tap);
  font-size:var(--fs-note); font-weight:var(--w-strong);
  color:var(--dimmer); text-align:center;
}
.fbg .fbg-alt a{color:var(--accent-ink); margin-left:4px}

/* ---- the offer ----------------------------------------------------------- */

/* THE STRIP OF COVERS — what stands where a statistic was asked for.

   js/recommend.js builds it: one band, aria-hidden, holding the whole
   catalogue's covers twice over in one track. WHAT IT REPLACED was the proof
   slot, a tinted box with two lines of copy in it, sitting where the mockup
   wanted two percentages about "Factbox members" that no study supports. The
   covers assert nothing; they are the product.

   THE ANIMATION MOVES ONE PROPERTY. translate3d on the track, 0 to -50%, and
   the two halves are identical so the wrap is invisible. Nothing here reads
   or writes layout per frame — no width is measured, no offset is set — which
   matters because this runs inside an in-app webview on the screen that takes
   money. The duration is long rather than the distance short: the band drifts
   at roughly a cover every two seconds, which reads as depth rather than as a
   carousel demanding to be watched.

   IT IS INVISIBLE UNTIL IT HAS COVERS. The strip is height:0 with nothing in
   it, so a sheet whose index never arrived has no gap in it — is-on is added
   by fillStrip() and nothing else turns it on.

   AND IT STOPS FOR prefers-reduced-motion, standing still with the same
   covers in it. Depth does not need movement to read; a reader who has asked
   the OS for stillness on the one screen they are deciding on gets it. */
.fbg-strip{
  height:0; overflow:hidden;
  margin:0;
  /* The band fades out at both ends rather than being cut off square, so the
     covers arrive and leave instead of appearing. currentColor, not a hex:
     a mask only reads the alpha, and NOTHING IN THIS FILE NAMES A COLOUR. */
  -webkit-mask-image:linear-gradient(to right, transparent, currentColor 12%,
                                     currentColor 88%, transparent);
  mask-image:linear-gradient(to right, transparent, currentColor 12%,
                             currentColor 88%, transparent);
}
.fbg-strip.is-on{height:auto; margin:14px 0 0}
.fbg-track{
  display:flex; flex-direction:row; align-items:center;
  gap:8px; width:max-content;
  will-change:transform;
  animation:fbg-roll 120s linear infinite;
}
.fbg-cover{
  flex:0 0 auto;
  display:block; width:46px; height:60px;
  border-radius:8px; overflow:hidden;
  background:var(--tint);
  box-shadow:inset 0 0 0 1px var(--hair);
}
.fbg-cover img{
  display:block; width:100%; height:100%;
  object-fit:cover;
}
@keyframes fbg-roll{
  from{transform:translate3d(0,0,0)}
  to  {transform:translate3d(-50%,0,0)}
}
/* Stillness, on request. The band keeps its covers. */
@media (prefers-reduced-motion:reduce){
  .fbg-track{animation:none}
}

/* The error note under the button — an "it did not work" line, not part of
   the offer. js/recommend.js inserts it above the two ghosts. */
.fbg .fbg-note{
  margin:4px 0 0; color:var(--dimmer);
  font-size:var(--fs-fine); font-weight:var(--w-body); line-height:1.45;
}

/* ---- THE PRICE, AND THE TWO THINGS IT HAS TO DO -------------------------

   .fbrate  the daily rate, in the treatment /join's plan cards use: the
            currency mark small at the shoulder, the whole units very large,
            the cents small and raised at the top right, PER DAY beneath
            them. It is deliberately NOT inside a .fbg selector — the plan
            picker (.pw-opt, further down) mounts on document.body, outside
            the wall, and both screens must set the same figure the same way.
   .fbg-price  the amount Stripe actually takes, in words, directly under it.

   THE SECOND ONE IS NOT DECORATION AND DOES NOT GIVE WAY. A reader
   authorises US$36.50 once a year, not ten cents; the daily figure is a
   division of it, rounded up in js/account.js so it can never quote under
   the real rate. Every max-height step at the foot of this file drops the
   covers and the currency note before it touches either of these lines.

   FLEX-DIRECTION IS PINNED, ON BOTH BOXES. This cluster was first built on
   /join as .jn-day, which is also the streak badge in css/account.css: a
   46x46 tile with flex-direction:column. It inherited all of it, the figures
   stacked and overlapped the line below, and every content assertion passed
   while it looked like that. The axis is stated here rather than left to the
   initial value, and tools/check-regressions.js guards the name. */
.fbrate{
  display:flex; flex-direction:row; align-items:flex-start;
  gap:6px; margin:0;
  color:var(--parchment);
  font-variant-numeric:tabular-nums;
}
.fbrate-was{
  align-self:center; margin-right:2px;
  font-size:var(--fs-meta); font-weight:var(--w-label); color:var(--dimmer);
}
.fbrate-cur{
  align-self:flex-start; margin-top:.5em;
  font-size:var(--fs-note); font-weight:var(--w-head);
}
.fbrate-big{
  font-size:var(--fs-hero); font-weight:var(--w-head);
  line-height:.92; letter-spacing:var(--ls-hero);
}
.fbrate-tail{
  display:flex; flex-direction:column; align-items:flex-start;
  align-self:flex-start; margin-top:.12em;
}
.fbrate-sup{font-size:var(--fs-sub); font-weight:var(--w-head); line-height:1}
.fbrate-unit{
  margin-top:2px;
  font-size:var(--fs-micro); font-weight:var(--w-label);
  letter-spacing:var(--ls-caps); text-transform:uppercase; color:var(--dim);
}
/* ON THE WALL IT IS THE HEADLINE, and it is set at headline size — bigger
   than /join's card sets it, because a card on a plan screen is one of two
   things being compared and this is the only figure on the screen. The
   proportions are /join's: the mark at the shoulder, the cents at the top
   right at roughly a third of the digit, PER DAY under them. */
.fbrate.is-lead{justify-content:center; margin:16px 0 0}
.fbrate.is-lead .fbrate-big{font-size:clamp(2.75rem,13vw,3.5rem)}
.fbrate.is-lead .fbrate-cur{font-size:var(--fs-sub); margin-top:.85em}
.fbrate.is-lead .fbrate-sup{font-size:var(--fs-h3)}
.fbrate.is-lead .fbrate-tail{margin-top:.2em}
/* In a plan row it is one line of a choice, so it sits at the row's own left
   edge and comes down a size — the row is a comparison, not a hero. */
.fbrate.is-row{justify-content:flex-start; margin:4px 0 0}
.fbrate.is-row .fbrate-big{font-size:var(--fs-h1); line-height:.95}
.fbrate.is-row .fbrate-cur{font-size:var(--fs-fine); margin-top:.45em}
.fbrate.is-row .fbrate-sup{font-size:var(--fs-note)}

/* The charge itself: "US$36.50 billed once a year", from FBA's billedText
   and its own word for the cycle. Never abbreviated, never dropped. */
.fbg .fbg-price{
  margin:6px 0 0; text-align:center;
  color:var(--dim);
  font-size:var(--fs-note); font-weight:var(--w-label);
  font-variant-numeric:tabular-nums;
}

.fbg .fbg-go{margin-top:16px; line-height:1.2}
/* The terms, under the button, in the order they happen: the trial, what
   today costs, and that it stops when the reader says so. */
.fbg .fbg-fine{margin:9px auto 0; text-align:center}

.fbg-ghosts{
  display:flex; flex-wrap:wrap; align-items:center; justify-content:center;
  gap:2px; margin-top:2px;
}
.fbg .fbg-plans,
.fbg .fbg-later{
  display:block; width:auto; margin:0;
  border:0; background:none; cursor:pointer;
  font-family:var(--ui); font-size:var(--fs-note); font-weight:var(--w-strong);
  padding:10px 12px; min-height:var(--tap);
}
.fbg .fbg-plans{color:var(--accent-ink)}
.fbg .fbg-later{color:var(--dimmer)}
/* "Not now" on the account sheet is the same control in the same place. */
.fbg .fbg-auth .fbg-later{
  display:block; width:100%; margin-top:2px; text-align:center;
}

/* Short phones, then a phone turned sideways.

   THE ORDER THINGS GIVE WAY IN IS THE ORDER OF WHAT THEY ARE FOR. Breathing
   room first. Then the strip of covers and the currency note, which are the
   two things on the offer that are neither the offer nor the terms. Then the
   sentence under the headline. THE PRICE — BOTH LINES OF IT, the daily rate
   and the amount actually charged — the button and the terms never go, at any
   height, because those are what the reader is agreeing to.

   The 620px step is set where it is on purpose: a 320x568 phone is the
   smallest viewport this site sees and 568 has to clear it, so the step sits
   above 568 rather than at a round number below it. */
@media (max-height:700px){
  .fbg-sheet{padding-top:18px}
  .fbg .fbg-head{font-size:1.625rem}
  .fbg .fbg-headbig{font-size:1.875rem}
  .fbg-strip.is-on{margin-top:10px}
  .fbg-cover{width:42px; height:54px}
  .fbrate.is-lead{margin-top:12px}
  .fbrate.is-lead .fbrate-big{font-size:2.5rem}
  .fbrate.is-lead .fbrate-sup{font-size:var(--fs-lead)}
  .fbg .fbg-go{margin-top:12px}
}
@media (max-height:620px){
  .fbg .fbg-head,
  .fbg .fbg-headbig{font-size:1.4375rem}
  .fbg .fbg-sub{font-size:var(--fs-note)}
  .fbg-strip{display:none}
  .fbg .fbg-cur{display:none}
  .fbg .fbg-btn{min-height:var(--tap); margin-top:6px}
  .fbg .fbg-google{margin-top:12px}
  .fbg .fbg-go{margin-top:10px}
  .fbg .fbg-fine{margin-top:6px}
}
/* Sideways. 390px of height, of which the in-app toolbar takes 64: the
   sentence under the headline goes, the eyebrow goes with it (the story is
   right there behind the sheet), and the two ghosts share their line. */
@media (max-height:460px){
  .fbg-sheet{padding-top:14px; max-height:96vh}
  .fbg .fbg-eyebrow{display:none}
  .fbg .fbg-head,
  .fbg .fbg-headbig{font-size:1.3125rem}
  .fbg .fbg-sub{display:none}
  .fbg .fbg-btn{margin-top:5px}
  .fbg .fbg-google{margin-top:10px}
  .fbrate.is-lead{margin-top:8px}
  .fbrate.is-lead .fbrate-big{font-size:var(--fs-h1)}
  .fbrate.is-lead .fbrate-sup{font-size:var(--fs-sub)}
  .fbrate.is-lead .fbrate-cur{font-size:var(--fs-note); margin-top:.5em}
  .fbg .fbg-go{margin-top:8px}
  .fbg .fbg-alt,
  .fbg .fbg-plans,
  .fbg .fbg-later{min-height:var(--tap); padding-top:6px; padding-bottom:6px}
}

/* ==========================================================================
   2b. THE BOUNDARY, in the story itself.

   read.html draws the free run of a locked story as ordinary cards, and adds
   two things to the last of them: the next card's opening line at the
   opacity of something you cannot read yet, and a hairline with a padlock in
   it. Then a tail below the last card — the room the gesture needs, and the
   one way through for a reader whose sheet never opened.

   THE PADLOCK IS INLINE SVG, NOT AN EMOJI. The mockup draws it as one; an
   emoji beside this typography is somebody else's artwork, at somebody
   else's colour, and it is the one glyph on the screen a reader cannot
   restyle.

   Named fbw-, not lock-: css/app.css already styles .lock, and a class that
   collides with a styled one is how a whole page ended up centred with every
   check green.
   ========================================================================== */

/* ALPHA, NOT OPACITY, AND THE REASON IS TWO REASONS.

   css/app.css's own note: alpha rather than opacity, because opacity fades the
   text-shadow the words are relying on at the same time as the words. And
   js/scenes.js adds `.rise` to every <p> inside a .copy, this one included, so
   the caption choreography ENDS at `opacity:1` — an opacity set here would be
   overwritten by the animation a beat after the card goes live, which is a
   bug that only appears once the reader has stopped looking for it.

   So the step down the ladder is a colour: the head is full cream, the body is
   --dim (.88), and the line the reader cannot have yet is --dimmer (.62), the
   same alpha every other quiet line in the reader uses. It keeps the rise, so
   it arrives with the rest of the card rather than being there before it. */
.beat .fbw-peek{
  margin:16px 0 0;
  color:var(--dimmer);
  font-size:var(--fs-lead); font-weight:var(--w-body); line-height:1.62;
  text-wrap:pretty;
  /* It sits BEHIND the card rather than on it, so it gives up the shadow the
     rest of the copy is lifted by. */
  text-shadow:none;
  /* Two lines of it, then it stops. This is a taste of the next card, not
     the next card. */
  display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2;
  overflow:hidden;
}
.beat .fbw-rule{
  display:flex; align-items:center; gap:12px;
  margin:18px 0 0;
}
.beat .fbw-rule i{
  flex:1 1 auto; height:1px; display:block;
  background:var(--accent-edge);
}
.beat .fbw-rule span{
  flex:0 0 auto; width:15px; height:15px; display:block;
  color:var(--accent-ink);
}
.beat .fbw-rule svg{width:100%; height:100%; display:block}

/* The tail. Tall enough that one more scroll is a real gesture rather than a
   twitch, and it carries a real link: if the scroll watcher never armed — no
   listeners, a webview that swallows the events, a reader on a keyboard who
   got here another way — this is the way through, and it works with
   JavaScript switched off because it is an <a> with an href. */
/* Proximity, not mandatory, and only while a locked run is on screen.

   css/app.css gives .deck `scroll-snap-type: y mandatory`, which is correct
   for a story: every card is a full screen and the reader lands on one rather
   than between two. Under a boundary it is wrong. Mandatory snapping makes
   the last card the last snap point the scroller will rest on, so the tail
   below it can never be reached, the way-through link in it can never be
   seen, and the one-more-scroll gesture is undone by the engine before it
   finishes. Proximity keeps the card-to-card snap and allows a rest at the
   foot of the scroller.

   This ADDS a modifier; it does not redefine .deck. read.html puts the class
   on only for a locked run. */
.deck.is-wall{scroll-snap-type:y proximity}

.fbw-gap{
  position:relative;
  min-height:210px;
  display:flex; flex-direction:column;
  align-items:center; justify-content:center;
  padding:24px 22px var(--bottom-safe);
  background:var(--ground);
  text-align:center;
}
.fbw-gap p{
  margin:0 0 6px; max-width:32ch;
  color:var(--dimmer);
  font-size:var(--fs-note); font-weight:var(--w-body); line-height:1.5;
}

/* ==========================================================================
   2c. "View other plans" — the sheet.

   Exactly the rungs js/account.js still offers, which is two: annual, marked
   best value and selected on open, and monthly. Quarterly is retired with
   offered:false over there and never reaches this file, so there is nothing
   here to keep in step.

   Fixed, and appended to <body> rather than to the pane, because the pane
   scrolls and a sheet inside a scrolling box scrolls away with it.
   ========================================================================== */
.pw-sheet{
  position:fixed; top:0; right:0; bottom:0; left:0;
  z-index:60;
  display:flex; flex-direction:column; justify-content:flex-end;
}
.pw-sheetbg{
  position:absolute; top:0; right:0; bottom:0; left:0;
  background:var(--glass-deep);
}
.pw-sheetcard{
  position:relative;
  width:100%; max-width:520px; margin:0 auto;
  padding:16px 18px calc(var(--bottom-safe) + 8px);
  border-radius:20px 20px 0 0;
  background:var(--menu);
  box-shadow:var(--shadow-2);
  -webkit-backdrop-filter:blur(18px);
  backdrop-filter:blur(18px);
  max-height:88vh; overflow-y:auto;
  text-align:left;
}
.pw-sheethead{
  margin:0 0 12px;
  font-size:var(--fs-sub); font-weight:var(--w-head);
  letter-spacing:var(--ls-title); color:var(--parchment);
  text-align:center;
}

.pw-opt{
  position:relative;
  display:block; width:100%; margin:0 0 8px;
  padding:12px 44px 12px 14px;
  border:0; border-radius:14px; cursor:pointer;
  background:var(--tint); box-shadow:inset 0 0 0 1px var(--hair);
  font-family:var(--ui); text-align:left;
  min-height:var(--tap);
}
.pw-opt.is-on{
  background:var(--accent-wash);
  box-shadow:inset 0 0 0 2px var(--accent-edge);
}
.pw-optline{display:flex; align-items:center; gap:8px}
.pw-optline b{
  font-size:var(--fs-sub); font-weight:var(--w-head);
  letter-spacing:var(--ls-title); color:var(--parchment);
}
.pw-best{
  font-size:var(--fs-micro); font-weight:var(--w-head);
  letter-spacing:var(--ls-badge); text-transform:uppercase;
  padding:3px 7px; border-radius:var(--pill);
  background:var(--coral); color:var(--on-accent);
}
/* The charge, under the daily rate, on every row. The rate above it is
   .fbrate.is-row — the same cluster the wall leads with, one size down. The
   per-month line that used to sit here is gone: it only ever appeared on
   multi-month rungs, so the two rows were quoted in different units from each
   other, and from the screen the reader had just come from. */
.pw-optprice{
  display:block; margin-top:2px;
  font-size:var(--fs-note); font-weight:var(--w-label); color:var(--dim);
  font-variant-numeric:tabular-nums;
}
.pw-tick{
  position:absolute; top:50%; right:13px;
  margin-top:-11px;
  width:22px; height:22px; border-radius:var(--pill);
  display:flex; align-items:center; justify-content:center;
  background:var(--tint-3); color:var(--menu);
}
.pw-tick svg{width:13px; height:13px; display:block}
.pw-opt.is-on .pw-tick{background:var(--coral); color:var(--on-accent)}

.pw-sheetgo{margin-top:12px; line-height:1.2}
.pw-sheetclose{
  display:block; width:100%; margin:2px auto 0;
  border:0; background:none; cursor:pointer;
  font-family:var(--ui); font-size:var(--fs-sub); font-weight:var(--w-strong);
  color:var(--dim); padding:10px; min-height:var(--tap);
}

/* Reduced motion. app.css already kills every transition on the page with an
   !important, so the four screens cut rather than fade — which is the right
   answer and is why nothing here has to repeat it. The advance itself is NOT
   disabled: what that setting asks for is less movement, not fewer features,
   and the countdown's only cue is text. */
@media (prefers-reduced-motion:reduce){
  .pane.rec,
  .pane.paywall{scroll-behavior:auto}
  /* With transitions off, `visibility 0s linear .45s` would still hold the
     outgoing screen for most of half a second. */
  .ec-p1, .ec-p2{transition:none}
  .fbg-sheet, .fbg-veil{transition:none}
}
