/* ==========================================================================
   Factbox — library + reader
   Phone-first. All traffic arrives from Instagram / TikTok, so every reader is
   in an in-app webview: a viewport that resizes mid-scroll and a toolbar that
   eats the bottom 44-60pt. Both facts are designed around, not worked around.

   TWO GROUNDS, ONE SET OF TOKENS.
   The browse pages are paper: cream, navy type, blue fills. The READER is
   night, because every pane there is a museum painting filling the screen with
   words laid over it, and cream behind a photograph is unreadable whatever
   colour the rest of the site is. Both are painted explicitly rather than
   inherited from the host webview.

   The reader's night values are ONE `:root` block, and it lives in
   css/reader-rail.css — read.html loads that file last and no other page loads
   it at all, so it is the reader's scope with no class on <html> to set and no
   :has() to depend on. If you add a page that needs night, load that file.
   The composed story pages (story.html, cleopatra.html, firststory.html) DO
   load this stylesheet — they are composed from read.html, which loads it, and
   then reader-rail.css repaints :root on top. They were once self-contained;
   they have not been for some time, and the note said otherwise until a
   palette repaint made it matter. It is the reason the reader came through
   that repaint intact: it inherits these tokens and then overrides the ones
   the night ground needs.
   ========================================================================== */

:root{
  color-scheme:light;

  /* ---- Paper -------------------------------------------------------------
     Six colours: cream, navy, bright blue, light blue, sage, dark brown.

     The token NAMES are kept even though their meaning inverted — --parchment
     is the reading colour and is now navy, not near-white. Renaming it would
     have touched several hundred declarations across nine stylesheets for no
     gain; the value is the thing that matters and this note is the warning.

     --coral is a FILL, never type. At 2.2:1 on cream it is a colour you put
     words on, not a colour you write words in. --accent-ink is the same blue
     taken down until it clears on the page, on a card AND on the washes, and
     it is what every link, eyebrow and coloured label uses.

     THE TWO INKS ARE MEASURED ON THE WASHES, NOT ONLY ON THE GROUNDS.
     Both used to stop at --ground and --raise, which is the mistake the note
     below --dim describes and did not finish. A wash is a fill laid over the
     page, so it is LIGHTER than --ground for --accent-wash and DARKER for the
     sage one, and a token tuned on the page alone lands under 4.5:1 the moment
     something is written on either. Both were taken down until the worst of
     the four surfaces clears 4.5:1. The four, measured:

       --accent-ink #1A5BA1   ground 5.24  raise 5.79  --accent-wash 4.60
                              --menu 6.51

     The mockup's own blue is #1B5FA8, which measures 4.92 on the page and
     4.32 on --accent-wash — under. #1A5BA1 is that colour taken down 4%,
     which is invisible side by side and clears the wash at 4.60.
       --teal-ink   #3B5119   ground 6.07  raise 7.24  --accent-wash 5.43
                              sage wash rgba(140,167,93,.30) 5.09

     The sage wash is the binding constraint on --teal-ink and --accent-wash on
     --accent-ink. Anything new laid on a wash is measured on the wash.
     ---------------------------------------------------------------------- */
  --parchment:#172E5B;                 /* navy — the reading colour */
  --ink:#172E5B; --ink-soft:#3C4E71;   /* navy, and it softened: 10.1 and 6.4
                                          on the page, 11.2 and 7.0 on a card */
  --coral:#3B9EF4;                     /* bright blue — fills and buttons */
  --coral-deep:#1B5FA8;
  --accent-ink:#1A5BA1;                /* the blue you may WRITE in */
  --crimson:#A81F38;                   /* the one colour that means STOP: 5.48:1
                                          on the page, 6.06:1 on a card. Both
                                          figures re-measured after the repaint
                                          moved --ground and --raise; the old
                                          comment said 4.94/5.90, which was true
                                          of the cream this palette replaced. */
  --teal:#8CA75D;                      /* sage — the second accent, as a fill */
  --teal-ink:#3B5119;                  /* sage you may write in: 6.1:1 on cream,
                                          5.1:1 on its own .30 wash */
  --butter:#95C8F7;                    /* light blue — bars and washes */
  --lilac:#B79CED;
  /* Not repainted. --night is the backstop BEHIND a plate — .beat and .tdy-art
     show it for the milliseconds before the painting decodes, and a cream flash
     there is the one thing worse than a dark one. */
  --night:#0F1420;

  --ground:#E7E0D3;                    /* cream — the page */
  --raise:#F1EBDF;                     /* a card lifted off it */
  /* The transparent end of a fade over --ground / --raise. `transparent` is
     rgba(0,0,0,0) and older webviews interpolate it un-premultiplied, which
     greys the middle of every gradient on a light ground. */
  --ground-0:rgba(231,224,211,0);
  --raise-0:rgba(241,235,223,0);
  --glass-0:rgba(9,12,22,0);      /* the clear end of a scrim over a plate */
  --hair:rgba(23,46,91,.14);

  /* --dim and --dimmer are NOT her .66 / .5.

     On night, .62 and .52 of near-white over #0E0B12 measure 8.4:1 and 6.2:1.
     Inverted onto cream the same alphas measure 3.8:1 and 2.6:1, and every one
     of their users — .meta under a cover, .fine under the buy button, an
     unselected tab, .statline small — is 12-15px body text that needs 4.5:1.
     Raised until they clear it on the WORSE of the two grounds (#E7E0D3):
     .82 → 6.4:1 on the page and 6.8:1 on a card, .74 → 5.1:1 and 5.4:1.
     They read closer together than they do on night. That is the honest cost
     of a light ground, and it is cheaper than illegible metadata. */
  --dim:rgba(23,46,91,.82);
  --dimmer:rgba(23,46,91,.74);

  /* ---- Surfaces that are not the page ------------------------------------
     Every one of these was a hard-coded near-white or near-black rgba() before
     the repaint, which is exactly why they all broke at once: a wash written
     as rgba(255,247,237,.09) is "a little lighter than whatever is behind it",
     and on cream that is a little lighter than cream.
     ---------------------------------------------------------------------- */
  --tint:rgba(23,46,91,.06);           /* a quiet chip on --raise */
  --tint-2:rgba(23,46,91,.12);         /* the same chip, pressed */
  --tint-3:rgba(23,46,91,.18);         /* an unfilled track or dot */
  --accent-wash:rgba(59,158,244,.16);  /* a chosen option's ground */
  --accent-edge:rgba(27,95,168,.32);   /* its hairline */
  --on-accent:#06182E;                 /* type on a --coral fill — 6.3:1 */
  --menu:#FAF6EE;                      /* the account popover, and the notice */

  /* Glass over ARTWORK stays dark in both grounds: the lock badge and the
     reader's own pills sit on a painting, never on the page. --on-glass is
     what you may write on them (8.4:1 over the lock badge's composite). */
  --glass:rgba(9,12,22,.62);
  --glass-deep:rgba(9,12,22,.72);
  --on-glass:#F4EFE6;
  /* The quieter line on the same glass: a source credit, never a sentence.
     Alpha rather than opacity, because opacity would fade the text-shadow the
     words are relying on at the same time. */
  --on-glass-soft:rgba(244,239,230,.76);

  /* ---- The focus ring ----------------------------------------------------
     ONE token, because four rules each picked their own colour and three of
     them picked --coral, which the note at the top of this file calls a FILL,
     never type — and a 2px ring is thinner than type. Measured on --ground it
     was 2.01:1 against a 3:1 requirement.

     --ring is a var() indirection on purpose. css/reader-rail.css repaints
     --accent-ink for night (#FF7A5C, 6.5:1 on #0E0B12), and a custom property
     resolves where it is USED, so the reader gets its own ring from this one
     declaration with nothing to keep in step.

     --ring-night is for the one surface that is night INSIDE a paper page:
     the Explore mosaic, whose ground is --night behind the tiles. Set --ring
     on any such container and everything focusable inside it inherits the
     right ring — see .tdy-mosaic below. Measured:

       --ring (=--accent-ink)  --ground 5.52  --raise 6.58  --menu 7.16
       --ring-night            --night 10.40
     ---------------------------------------------------------------------- */
  --ring:var(--accent-ink);
  --ring-night:var(--accent-lift);

  /* Shadow is a light source, and on cream the light is coming from the same
     place — so these are navy at low alpha, not black at .45, which on paper
     reads as soot around every cover. */
  --shadow-1:0 6px 20px rgba(23,46,91,.14);   /* a cover lifted off the page */
  --shadow-2:0 14px 40px rgba(23,46,91,.22);  /* a menu or a notice above it */
  --shadow-go:0 8px 22px rgba(27,95,168,.26); /* the primary button's own */
  --glow-accent:0 0 10px rgba(59,158,244,.5);/* the read bar's trail */
  --accent-lift:#8FC4FA;                      /* the sweep along the load bar */

  /* ---- Type --------------------------------------------------------------
     DM Sans, four real weights. Every page's <head> asks Google for
     wght@400;500;600;700 and gets those four faces back, so the 650 / 750 /
     780 / 820 / 850 / 900 values this stylesheet used to carry were not
     weights the browser had: each snapped to the nearest face or was
     synthesised, which is why 800 and 850 rendered identically and 650 came
     out as 600 on iOS and as faux-bold on Android. Four tokens, one job each.

       --w-head    700   headlines, card titles, the word that is the point
       --w-strong  600   buttons, the current tab, a <b> inside a paragraph
       --w-label   500   metadata, counts, eyebrows, small-caps labels
       --w-body    400   paragraphs

     The fallbacks are the rounded stack DM Sans replaced, so a webview that
     blocks the font request still gets a face rather than Times. The link
     carries display=swap, because the words are the product: they appear
     immediately in the fallback instead of after a round trip on cellular.

     And one size ladder. This file alone held eleven distinct sizes between
     .64rem and .82rem; these are the rungs they collapse to. rem, not px, so
     the ladder tracks the reader's own text size.
     ---------------------------------------------------------------------- */
  /* The display serif. Newsreader at 500 sets the headline and
     Today's hook; every other word on the site is DM Sans. Any page
     that uses it must add Newsreader to its own Google Fonts link -
     a token cannot fetch a font. */
  --display:"Newsreader",Georgia,"Times New Roman",serif;
  --w-display:500;

  --ui:"DM Sans",ui-rounded,"SF Pro Rounded","Nunito",system-ui,-apple-system,"Segoe UI",sans-serif;

  --w-head:700; --w-strong:600; --w-label:500; --w-body:400;

  --fs-hero:clamp(1.75rem,7vw,2.5rem);   /* the mast. 28px at 390, which is
                                            the mockup's figure; 7vw takes over
                                            above 400px. NOT Explore's headline —
                                            css/today.css gives that its own serif
                                            clamp and does not read this. */
  --fs-h1:clamp(1.6rem,6.4vw,2.1rem);    /* a pane's headline */
  --fs-h2:clamp(1.35rem,5.2vw,1.7rem);   /* a card headline */
  --fs-h3:1.3rem;                        /* a section divider */
  --fs-lead:1.0625rem;                   /* a standfirst, a primary button */
  --fs-body:1rem;                        /* paragraphs, inputs */
  --fs-sub:.9375rem;                     /* a card title, a tab, a menu item */
  --fs-note:.875rem;                     /* secondary copy */
  --fs-meta:.8125rem;                    /* metadata under a cover */
  --fs-fine:.75rem;                      /* the fine print */
  --fs-micro:.6875rem;                   /* a tag, an eyebrow, a badge */

  --ls-hero:-.02em;                      /* the bigger the type, the tighter */
  --ls-head:-.015em;
  --ls-title:-.008em;
  --ls-caps:.16em;                       /* anything set in small capitals */
  --ls-badge:.08em;                      /* a filled badge, where .16em splits the pill */
  --ls-mark:.32em;                       /* the wordmark, and only it */

  --lh-head:1.12; --lh-body:1.55; --lh-tight:1.25;

  /* The bottom of an in-app webview is not ours. Nothing tappable and nothing
     that must be read sits below this line. */
  --toolbar:60px;
  --bottom-safe:max(13vh, calc(env(safe-area-inset-bottom,0px) + var(--toolbar) + 4px));
  --top-safe:calc(env(safe-area-inset-top,0px) + 12px);

  --tap:44px;
  --pill:999px;
}

*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{margin:0;padding:0}
body{
  background:var(--ground);
  color:var(--parchment);
  font-family:var(--ui);
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}
a{color:inherit}
img{max-width:100%;display:block}
:focus-visible{outline:2px solid var(--ring);outline-offset:3px;border-radius:6px}

/* The Explore mosaic is a wall of paintings on --night, inside a cream page.
   Set the ring here, on the container, and every tile inherits it: the ring is
   the only thing on that wall that is not artwork, and --accent-ink — correct
   everywhere else on this page — measured 2.52:1 against the mosaic's own
   ground. This declaration lives in app.css rather than css/today.css because
   the ring is app.css's to own; today.css paints the wall. */
.tdy-mosaic{--ring:var(--ring-night)}

/* ==========================================================================
   Library
   ========================================================================== */

.lib{
  min-height:100vh; min-height:100dvh;
  /* The +96px used to reserve room for the buy bar fixed to the foot of the
     shelf, so it never covered the last row. There is no buy bar any more, on
     any page, and the reservation became 96px of dead space under the last
     cover — measured at 270px of whitespace on a 430px phone.

     It was only ever right on one of the four pages that use .lib: the home
     page, Explore and Library never had a bar, so they carried the gap for
     nothing from the day this rule was written. --bottom-safe stays, because
     that is the Instagram and TikTok toolbar, which is still there. */
  padding:var(--top-safe) 18px var(--bottom-safe);
  max-width:900px; margin:0 auto;
}

.mast{padding:22px 2px 6px}
.mark{
  font-size:var(--fs-meta); font-weight:var(--w-head); letter-spacing:var(--ls-mark);
  color:var(--accent-ink); margin:0 0 14px;
  /* A flex row so the tile sits on the wordmark's optical centre rather than
     on its baseline, where a 24px square hangs a descender's worth low. The
     row is the same height as the text alone on the ten pages that carry no
     tile, so this costs those pages nothing. */
  display:flex; align-items:center;
}
/* The wordmark is the way home. Tapping "Factbox" anywhere in the app goes to
   /explore, which is the shelf — the anchor lives INSIDE the <p> rather than
   replacing it so the header's box, its margin and its baseline are the ones
   that were already measured, and so the hit target is the word rather than
   the full width of the row.

   It is a wordmark, not a nav link: no underline, no link colour of its own,
   no visited state. The hover darkens to the reading colour (9.01:1 on cream)
   rather than lightening to --coral-deep, which measures 2.75:1 and would make
   the one thing on the page you are pointing at the least legible thing on it.
   The ring is the site's ring, from :focus-visible. */
.mark a{
  display:inline-flex; align-items:center;
  /* 7px, not 8. --ls-mark is .32em and letter-spacing is added AFTER every
     character, the last one included — so the gap the eye sees between the
     tile and the F is the flex gap plus nothing, while the gap after the X is
     the flex gap's worth of tracking. Measured side by side, 8px read wide;
     7px reads as the 8 it is meant to be. */
  gap:7px;
  color:inherit; text-decoration:none; border-radius:6px;
  /* The lockup is 24px tall — the logo's height, which the design sets. As a
     TARGET that is 24px, and this is the way back to Explore from twelve
     pages. Same treatment as .acct-btn: an invisible 44px-tall band centred
     on it, costing no layout, so the masthead's baseline does not move.
     Measured at 390: the band runs 24->68 and the first tab starts at 74, so
     it cannot steal a tap meant for Explore. */
  position:relative;
  transition:color .16s ease;
}
.mark a::after{
  content:""; position:absolute; left:0; right:0; top:50%;
  height:var(--tap); transform:translateY(-50%);
}

.mark a:hover{color:var(--parchment)}
/* No border-radius here. The PNG's corners are already rounded and
   transparent; a radius on top clips them a second time and flattens them.
   width/height are on the <img> tag as well, so the masthead reserves the box
   before the file lands. */
.mark img{
  flex:0 0 auto; width:24px; height:24px; display:block;
}
.mast h1{
  font-size:var(--fs-hero); font-weight:var(--w-head); margin:0 0 10px;
  line-height:1.08; letter-spacing:var(--ls-head); text-wrap:balance;
}
/* :not(.mark), because the wordmark is a <p> in this same block and `.mast p`
   outranks `.mark` on specificity. It has been quietly winning: FACTBOX was
   rendering at 16px in --dim rather than at --fs-meta in the accent, and
   giving this rule a font-weight would have taken the wordmark's weight too.
   css/today.css already writes the standfirst as `.mast p:not(.mark)` for the
   same reason. */
.mast p:not(.mark){
  margin:0; color:var(--dim); max-width:44ch;
  font-size:var(--fs-body); font-weight:var(--w-body); line-height:var(--lh-body);
}

.sechead{
  display:flex; align-items:baseline; gap:10px;
  margin:34px 2px 14px; padding-top:18px; border-top:1px solid var(--hair);
}
.sechead h2{
  margin:0; font-size:var(--fs-lead); font-weight:var(--w-head);
  letter-spacing:var(--ls-title);
}
.sechead span{font-size:var(--fs-meta); color:var(--dimmer); font-weight:var(--w-label)}

/* Two up on a phone, more as the window allows. The cover is the pitch, so it
   gets the room; the title sits under it rather than over it, because a title
   burned into a painting is unreadable half the time. */
.grid{
  display:grid; gap:16px 13px;
  grid-template-columns:repeat(auto-fill,minmax(148px,1fr));
}

.card{
  display:flex; flex-direction:column; gap:9px;
  text-decoration:none; color:inherit;
  transition:transform .18s ease;
}
.card:active{transform:scale(.975)}

.plate{
  position:relative; aspect-ratio:3/4; overflow:hidden;
  border-radius:14px; background:var(--raise);
  box-shadow:var(--shadow-1), inset 0 0 0 1px var(--hair);
}
.plate img{
  width:100%; height:100%; object-fit:cover; object-position:center 32%;
}
.card h3{
  font-size:var(--fs-sub); font-weight:var(--w-strong); margin:0;
  line-height:var(--lh-tight); letter-spacing:var(--ls-title);
}
.meta{
  font-size:var(--fs-meta); color:var(--dimmer); font-weight:var(--w-label);
  font-variant-numeric:tabular-nums;
}

/* Locked covers stay legible — the point is to want them. A dimmed cover with
   a small lock reads as "not yet"; a blurred one reads as broken. */
.card.locked .plate img{opacity:.52; filter:saturate(.65)}
.card.locked h3{color:var(--dim)}
.lock{
  position:absolute; right:8px; top:8px;
  width:26px; height:26px; border-radius:var(--pill);
  background:var(--glass-deep); color:var(--on-glass); backdrop-filter:blur(6px);
  display:grid; place-items:center; font-size:var(--fs-fine);
  box-shadow:inset 0 0 0 1px var(--hair);
}
/* .buybar was removed here. There is no buy bar on any page — .lib's own note
   above records the day its padding reservation went too — and no markup or
   script referenced either rule. tools/check-shelf.js still asserts the bar;
   that check is stale, not this deletion.

   .copy .eyebrow went the same way: nothing writes an .eyebrow inside a .copy.
   The eyebrows that do exist are .tdy-eyebrow and .ob-eyebrow, in their own
   stylesheets. */

.go{
  display:block; width:100%; max-width:520px; margin:0 auto;
  border:0; border-radius:var(--pill); cursor:pointer;
  background:var(--coral); color:var(--on-accent);
  font-family:var(--ui); font-size:var(--fs-lead); font-weight:var(--w-strong);
  padding:16px 20px; min-height:var(--tap);
  box-shadow:var(--shadow-go);
  transition:transform .15s ease, filter .15s ease;
}
.go:active{transform:translateY(1px) scale(.99)}
.go[disabled]{background:var(--raise); color:var(--dim); box-shadow:none; cursor:default}
.fine{
  margin:9px auto 0; max-width:520px; text-align:center;
  font-size:var(--fs-fine); font-weight:var(--w-body);
  color:var(--dimmer); line-height:var(--lh-body);
}

/* ==========================================================================
   Reader
   ========================================================================== */

.deck{
  height:100vh; height:100dvh;
  overflow-y:auto;
  scroll-snap-type:y mandatory;
  scroll-behavior:smooth;
  -webkit-overflow-scrolling:touch;
  overscroll-behavior-y:contain;
  scrollbar-width:none;
}
.deck::-webkit-scrollbar{display:none}

.beat{
  position:relative;
  height:100vh; height:100dvh;
  scroll-snap-align:start; scroll-snap-stop:always;
  content-visibility:auto;
  contain-intrinsic-size:100vw 100dvh;
  overflow:hidden;
  background:var(--night);
}
.beat .art{position:absolute; inset:0}
.beat .art img{
  width:100%; height:100%; object-fit:cover; object-position:center 34%;
}
/* Two scrims, not one: a light wash over the whole plate so white type never
   sits on a bright sky, and a deep foot so the text block has a real ground. */
.beat .art::after{
  content:""; position:absolute; inset:0;
  background:
    linear-gradient(to top, rgba(10,8,13,.94) 0%, rgba(10,8,13,.78) 38%,
                    rgba(10,8,13,.34) 62%, rgba(10,8,13,.38) 100%);
}

.copy{
  position:absolute; left:0; right:0; bottom:var(--bottom-safe);
  padding:0 20px;
  max-width:620px; margin:0 auto;
}
.copy h2{
  font-size:var(--fs-h2); font-weight:var(--w-head); margin:0;
  line-height:1.2; letter-spacing:var(--ls-head); text-wrap:balance;
  text-shadow:0 2px 18px rgba(0,0,0,.55);
}
.copy p{
  margin:12px 0 0; font-size:clamp(1rem,3.9vw,1.08rem);
  font-weight:var(--w-body); line-height:1.62;
  color:rgba(255,247,237,.88); max-width:42ch;
  text-shadow:0 2px 14px rgba(0,0,0,.5);
}
/* The hook is the whole screen's job, so it gets the size and nothing else
   competes with it. */
.beat.is-hook .copy h2{font-size:clamp(1.85rem,7.4vw,2.5rem); line-height:1.1}
.beat.is-hook .copy p{font-size:clamp(1.05rem,4.2vw,1.15rem)}
/* The landing wants air under it, not another paragraph crammed to the edge. */
.beat.is-landing .copy{bottom:calc(var(--bottom-safe) + 4vh)}

/* The source line under a card, and it is the whole point of this site that
   it does not read as the author's own words. It has to out-specify .copy p:
   `.cite` is (0,1,0) and `.copy p` is (0,1,1), so for as long as this rule was
   written as `.cite` it lost every one of its five declarations and every
   citation rendered at the prose size, weight and colour — measured 18.06px
   /400, identical to the paragraph above it, on all 20 cards carrying one.

   And .copy p is not the only thing it has to beat. `.beat.is-hook .copy p`
   is (0,3,1) and raises the hook card's prose to clamp(1.05rem,4.2vw,1.15rem)
   — that is where the 18.06px the audit measured came from, on a 430px phone.
   So the rule is written twice: (0,2,1) for every ordinary card and (0,4,1)
   for the hook, which is the card whose citation is read first.

   text-shadow is deliberately NOT re-declared: .copy p's scrim still applies
   to this element and the words need it as much as the prose does. */
.copy p.cite,
.beat.is-hook .copy p.cite{
  margin:14px 0 0; font-size:var(--fs-fine); font-weight:var(--w-label);
  line-height:var(--lh-body); letter-spacing:var(--ls-title);
  color:var(--on-glass-soft);
}
.copy p.cite a,
.beat.is-hook .copy p.cite a{color:var(--on-glass); text-decoration:underline; text-underline-offset:2px}

/* Artwork credit. Small, always present, never in the way. Licence terms on
   the non-public-domain plates require it, and it costs one line. */
.plate-credit{
  position:absolute; left:20px; right:20px;
  bottom:calc(env(safe-area-inset-bottom,0px) + 8px);
  font-size:var(--fs-micro); line-height:1.4; color:rgba(255,247,237,.56);
  text-align:left;
}
.plate-credit a{color:rgba(255,247,237,.72)}

/* Where you are in the story.

   A column of clear segments down the right edge, one per card, the current
   one white and twice as tall — the same indicator the illustrated Cleopatra
   story has used since the beginning. It replaced a coral bar across the top
   of the viewport, which was drawn over the top edge of a full-bleed painting
   and could only say "some fraction in". This says how many cards there are
   and which one you are on.

   Vertically centred, so it never meets the action rail sitting on the floor
   of the same edge: thirteen cards make a column about 220px tall, and the
   rail's own top is 719px down a 932px screen. */
.vrail{
  position:fixed; right:7px; top:50%; transform:translateY(-50%);
  z-index:30; display:flex; flex-direction:column; gap:5px;
  pointer-events:none;
}
.vrail i{
  display:block; width:3px; height:12px; border-radius:var(--pill);
  background:rgba(255,255,255,.28);
  transition:background .3s ease, height .3s ease;
}
.vrail i.on{background:#fff; height:24px}

/* The end pane is the way out of the story, not a card in it — read.html
   marks the rail directly, the same way it marks the action rail. An earlier
   version of this rule keyed off a class on <html> that nothing ever set, so
   it silently did nothing. */
.vrail{transition:opacity .25s ease}
/* The rail is hidden on the end panes. display:flex outranks the UA's
   [hidden]{display:none}, so setting the attribute did nothing. */
.vrail[hidden]{display:none !important}
.vrail.is-off{opacity:0}

@media (prefers-reduced-motion: reduce){
  .vrail i{transition:none}
}

.topbar{
  position:fixed; top:var(--top-safe); left:12px; z-index:31;
  /* Boxes, not baselines. These are an <a> with words in it and a <button>
     with only an icon; aligned inline they sit on a shared text baseline, and
     the icon-only one has no text to put on it, so it hung low by a
     descender's worth. align-items:center is the whole fix. */
  display:flex; align-items:center; gap:8px;
}
/* The way out of a story. It sits on a painting, never on the page, so it is
   glass — and the two things that make glass legible are the scrim's depth and
   a colour that does not change with the palette.

   It used to take --parchment, which is the READING colour and inverts with
   the ground: near-white in the reader, navy on paper. Navy on a dark scrim is
   1.8:1, so the rule was one `<link>` away from unreadable and nothing in it
   said so. --on-glass is the token for exactly this — type on glass over
   artwork — and it is the same value on both palettes.

   --glass-deep rather than --glass because the scrim is what carries the
   words, not the colour: over the brightest plate in the corpus the .62 scrim
   composites to a floor of 5.22:1 and the .72 one to 7.58:1. Measured over the
   real plates it is 11.05:1 at its worst. */
.back{
  display:inline-flex; align-items:center; gap:7px;
  min-height:38px; padding:0 14px 0 11px; border-radius:var(--pill);
  background:var(--glass-deep); backdrop-filter:blur(10px);
  box-shadow:inset 0 0 0 1px var(--hair);
  font-size:var(--fs-meta); font-weight:var(--w-strong); text-decoration:none;
  color:var(--on-glass);
}

/* End-of-stack card and the paywall share a centred column. */
.pane{
  height:100vh; height:100dvh;
  scroll-snap-align:start;
  display:flex; flex-direction:column; justify-content:center;
  gap:16px; padding:var(--top-safe) 22px var(--bottom-safe);
  max-width:560px; margin:0 auto; text-align:center;
}
.pane h2{
  font-size:var(--fs-h1); font-weight:var(--w-head); margin:0;
  line-height:var(--lh-head); letter-spacing:var(--ls-head); text-wrap:balance;
}
.pane p{
  margin:0; color:var(--dim);
  font-size:var(--fs-body); font-weight:var(--w-body); line-height:1.62;
}
.pane .go{margin-top:8px}

.nextrow{
  display:flex; gap:12px; align-items:center; justify-content:center;
  margin-top:6px;
}
.nextrow .plate{width:78px; flex:0 0 78px; border-radius:11px}
.nextrow .t{text-align:left}
.nextrow .t b{
  display:block; font-size:var(--fs-sub); font-weight:var(--w-head);
  line-height:var(--lh-tight);
}
.nextrow .t span{font-size:var(--fs-meta); color:var(--dimmer)}

.ghost{
  display:inline-block; margin-top:4px;
  font-size:var(--fs-sub); font-weight:var(--w-strong); color:var(--dim);
  text-decoration:none; padding:10px; min-height:var(--tap);
}

/* A locked stack still shows its cover — that is the argument for paying. */
.paywall .plate{width:150px; margin:0 auto; border-radius:16px}

@media (prefers-reduced-motion:reduce){
  *{animation:none !important; transition:none !important}
  .deck{scroll-behavior:auto}
}

/* A few cards in the season are a single 150-200 character sentence with no
   body. The words are the author's, so the type gives rather than the copy:
   these set at body scale and read as a paragraph, not a shouted headline. */
.beat.is-long .copy h2{
  font-size:clamp(1.02rem,4vw,1.14rem);
  font-weight:var(--w-strong); line-height:1.45; letter-spacing:0;
  color:rgba(255,247,237,.93);
}
.beat.is-hook.is-long .copy h2{font-size:clamp(1.25rem,4.9vw,1.45rem); line-height:1.32}

/* --------------------------------------------------------------------------
   Reading state on a cover. Three states, and each has to read at a glance in
   a grid of fifty-one: untouched, part-read, finished.
   -------------------------------------------------------------------------- */
.readbar{
  position:absolute; left:0; bottom:0; height:3px;
  background:var(--coral); border-radius:0 3px 3px 0;
  box-shadow:var(--glow-accent);
}
/* Finished covers step back so the unread ones carry the eye. The check is the
   signal; dimming alone would read as "locked", which is a different thing. */
.card.is-done .plate img{opacity:.72}
.card.is-done .plate::after{
  content:"✓"; position:absolute; right:8px; bottom:8px;
  width:22px; height:22px; border-radius:var(--pill);
  background:var(--coral); color:var(--on-accent);
  font-size:var(--fs-fine); font-weight:var(--w-head); display:grid; place-items:center;
  box-shadow:0 2px 8px rgba(0,0,0,.4);
}
.card.is-reading .meta{color:var(--accent-ink); font-weight:var(--w-strong)}

/* "Pick up where you left off" — one wide cover, not a grid cell, because it
   is a single answer to a single question. */
.resume{
  display:flex; gap:14px; align-items:center;
  padding:12px; margin:18px 0 4px;
  border-radius:20px; background:var(--raise);
  box-shadow:inset 0 0 0 1px var(--hair);
  text-decoration:none; color:inherit;
}
.resume .plate{width:64px; flex:0 0 64px; border-radius:8px; aspect-ratio:3/4}
.resume .t{min-width:0}
.resume .t b{
  display:block; font-size:var(--fs-sub); font-weight:var(--w-head);
  line-height:var(--lh-tight);
}
.resume .t span{
  font-size:var(--fs-meta); color:var(--accent-ink); font-weight:var(--w-label);
}

/* --------------------------------------------------------------------------
   The restore link. Access lives in one browser's storage, so this link is the
   only bridge to another device — it has to be findable, not buried.
   -------------------------------------------------------------------------- */
.keep{
  margin:20px 0 4px; padding:16px;
  border-radius:16px; background:var(--raise);
  box-shadow:inset 0 0 0 1px var(--hair);
}
.keep b{
  display:block; font-size:var(--fs-sub); font-weight:var(--w-head); margin-bottom:5px;
}
.keep p{
  margin:0 0 11px; font-size:var(--fs-note); line-height:var(--lh-body); color:var(--dim);
}
.keep input{
  width:100%; margin-bottom:9px; padding:11px 12px;
  border:0; border-radius:10px; background:var(--tint);
  color:var(--parchment); font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:var(--fs-meta); min-height:var(--tap);
}
.keep button{
  width:100%; min-height:var(--tap); border:0; border-radius:var(--pill);
  background:var(--coral); color:var(--on-accent); cursor:pointer;
  font-family:var(--ui); font-size:var(--fs-sub); font-weight:var(--w-strong);
}
.keep .fine{margin:9px 0 0; text-align:left}

/* --------------------------------------------------------------------------
   Section nav. Three places: Home, Explore, Library.

   The selected tab is NOT a filled pill — it is the word, brighter, with a
   hairline under it. A coloured capsule reads as a button you have not pressed
   yet, which is the opposite of what a current-section marker should say.

   It sits at the top, not the bottom: the bottom 44-60pt of an in-app webview
   belongs to Instagram's toolbar.
   -------------------------------------------------------------------------- */
.tabs{
  display:flex; gap:22px; align-items:center;
  padding:2px 2px 0; margin:0 0 4px;
}
.tabs a{
  position:relative; text-decoration:none;
  padding:10px 0; min-height:var(--tap);
  display:inline-flex; align-items:center;
  font-size:var(--fs-sub); font-weight:var(--w-strong); letter-spacing:var(--ls-title);
  color:var(--dimmer);
  transition:color .16s ease;
}
.tabs a:hover{color:var(--dim)}
.tabs a[aria-current="page"]{color:var(--parchment); font-weight:var(--w-head)}
.tabs a[aria-current="page"]::after{
  content:""; position:absolute; left:0; right:0; bottom:4px; height:2px;
  border-radius:2px; background:var(--coral);
}
.tabs .count{
  margin-left:auto; font-size:var(--fs-meta); font-weight:var(--w-label); color:var(--dimmer);
  font-variant-numeric:tabular-nums;
}

/* The story's title as the page's one h1. Visible to assistive tech and to
   search, out of the way of a full-bleed reader that has no room for a header. */
.sr-title{
  position:absolute; width:1px; height:1px; margin:-1px; padding:0;
  overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap;
}

/* --------------------------------------------------------------------------
   The analytics notice. Sits above the in-app browser toolbar, says what is
   measured in one sentence, and goes away for good on a tap.
   -------------------------------------------------------------------------- */
.fbq-notice{
  position:fixed; left:10px; right:10px; z-index:60;
  bottom:calc(env(safe-area-inset-bottom,0px) + var(--toolbar) + 8px);
  display:flex; gap:12px; align-items:center;
  padding:12px 14px; border-radius:14px;
  background:var(--menu); backdrop-filter:blur(10px);
  box-shadow:var(--shadow-2), inset 0 0 0 1px var(--hair);
  font-size:var(--fs-meta); line-height:var(--lh-body); color:var(--dim);
  max-width:560px; margin:0 auto;
}
.fbq-notice a{color:var(--parchment)}
.fbq-notice button{
  flex:none; border:0; cursor:pointer; border-radius:var(--pill);
  background:var(--coral); color:var(--on-accent);
  font-family:var(--ui); font-size:var(--fs-meta); font-weight:var(--w-strong);
  padding:9px 14px; min-height:var(--tap);
}

/* The old account control is gone, and so are its rules.

   It was three links in the nav row — Sign in, a Sign up chip, and the
   reader's name — with `hidden` toggled between them, a truncation width per
   breakpoint, a floating variant for the reader, and a class on <html> that
   hid the lot until auth answered so it would not flash. Nine pages each
   carried their own copy of the script driving it.

   All of that is one circle now, drawn by js/acct.js, styled at the bottom of
   this file. Nothing here referenced it any more: .topbar-r, .acct.is-float,
   .acct a, .acct .acct-up, .acct .acct-me and html.fb-hdr had no users left
   in any page or script. A stylesheet full of rules for markup that no longer
   exists is the next person's afternoon. */

/* ==========================================================================
   The waiting state — js/loader.js

   A bar that moves and a line of copy, replacing "Loading…" everywhere. The
   whole thing is CSS: no script has to run for a reader to see it, which is
   why the bar animates on a keyframe rather than from JavaScript.

   The curve decelerates and stops at 92%. It is not measuring anything — a
   fetch of a JSON body has no honest progress to report — so it says the one
   true thing it can, which is "still going". A bar sitting at 100% while
   nothing happens reads as stuck; one still crawling reads as working.
   ========================================================================== */
.fbload{
  display:flex; flex-direction:column; gap:11px;
  margin-top:22px; max-width:26rem;
}
.fbload-track{
  height:4px; border-radius:999px; overflow:hidden;
  background:var(--tint-3);
}
.fbload-fill{
  display:block; height:100%; width:4%; border-radius:999px;
  background:linear-gradient(90deg,
    var(--coral,#3B9EF4) 0%, var(--accent-lift,#8FC4FA) 45%, var(--coral,#3B9EF4) 100%);
  background-size:220% 100%;
  /* 1.8s, not 3.2s. Pagination and the gate fix put first content on screen
     at around 900ms, so the old curve was still down at a third of the track
     when the content arrived — a bar that never got anywhere before it was
     replaced. This one is convincingly along by then and still has somewhere
     left to go if the network is having a bad day. */
  animation:fbload-grow 1.8s cubic-bezier(.1,.78,.2,1) forwards,
            fbload-shine 1.5s linear infinite;
}
@keyframes fbload-grow{
  0%{width:6%} 18%{width:42%} 45%{width:66%} 72%{width:82%} 100%{width:92%}
}

/* Finishing, driven by FBLoad.done(): the fill is run to 100% from script and
   the whole block fades once it lands. */
.fbload.is-done{opacity:0; pointer-events:none;
  transition:opacity 130ms ease 150ms}
@keyframes fbload-shine{ to{ background-position:-220% 0 } }

.fbload-say{
  margin:0; font-size:var(--fs-note); line-height:var(--lh-body);
  color:var(--dim,rgba(29,49,89,.82));
  font-variant-numeric:tabular-nums;
}

/* Inside the account and sign-in panels the bar sits in a narrow row, so it
   loses the top margin and the copy sits beside nothing else. */
.au-wait .fbload{margin-top:0; max-width:22rem}

/* Reduced motion: the bar still has to say "working", so it keeps one slow
   sweep and drops the shine, rather than freezing into a stuck-looking stub. */
html.no-motion .fbload-fill{
  animation:fbload-grow 4.5s linear forwards;
}
@media (prefers-reduced-motion: reduce){
  .fbload-fill{ animation:fbload-grow 4.5s linear forwards; }
}

/* ==========================================================================
   The account insignia — js/acct.js

   One circle, top right of every page that has a header, replacing the
   Sign in / Sign up pair that used to sit in the nav row. It is the same
   circle signed in or out, so nothing moves when auth answers a beat after
   first paint; only the letter inside it changes.
   ========================================================================== */
.acct{margin-left:auto; position:relative; display:flex; align-items:center}

.acct-btn{
  appearance:none;-webkit-appearance:none; cursor:pointer;
  width:34px; height:34px; padding:0; border:0; border-radius:50%;
  display:inline-flex; align-items:center; justify-content:center;
  background:var(--tint);
  box-shadow:inset 0 0 0 1px var(--hair);
  color:var(--parchment);
  font:var(--w-head) var(--fs-note)/1 var(--ui, inherit);
  -webkit-tap-highlight-color:transparent;
  transition:background .18s ease, box-shadow .18s ease, transform .12s ease;
  /* The disc is 34px because the design says 34px. The TARGET is 44px,
     because a thumb is a thumb: this pseudo-element is invisible, centred,
     and costs no layout — the row's height and the header's baseline are
     unchanged. Without it this is the only control on Explore under --tap. */
  position:relative;
}
.acct-btn::after{
  content:""; position:absolute; left:50%; top:50%;
  width:var(--tap); height:var(--tap); transform:translate(-50%,-50%);
}
.acct-btn:hover{background:var(--tint-2)}
.acct-btn:active{transform:scale(.94)}
/* No colour of its own. The ring is --ring, from the one rule at the top of
   this file; this used to name --coral and measured 2.01:1 on --ground. The
   circle keeps its border-radius because .acct-btn's own 50% is declared
   later in this file than :focus-visible's 6px. */
.acct-btn.is-in{
  background:var(--accent-wash);
  box-shadow:inset 0 0 0 1px var(--accent-edge);
  color:var(--accent-ink);
}

/* Signed out there is no letter, so the circle carries a plain figure drawn in
   CSS rather than an emoji — an emoji renders as somebody else's artwork at a
   different size on every platform, and this sits beside our own wordmark. */
.acct-ini.is-anon{
  width:13px; height:13px; position:relative; display:block;
}
.acct-ini.is-anon::before{
  content:""; position:absolute; left:50%; top:0; width:6.5px; height:6.5px;
  margin-left:-3.25px; border-radius:50%;
  box-shadow:inset 0 0 0 1.6px currentColor;
}
.acct-ini.is-anon::after{
  content:""; position:absolute; left:50%; bottom:0; width:13px; height:6.5px;
  margin-left:-6.5px; border-radius:7px 7px 0 0;
  box-shadow:inset 0 0 0 1.6px currentColor;
}

