/* ═══════════════════════════════════════════════════════════════════════════
 page.css — the project-page anatomy.

 The middle layer of the page kit. screens.css owns the CHOREOGRAPHY (fixed
 screens, the rail, transitions, galleries' built form, the scroll guard);
 this file owns the READING-ROOM ANATOMY every project page is built from —
 opening screen, section headers, tabs, panel interiors, content modules,
 closing screen, breakpoints. The page's own <style> owns only IDENTITY:
 the:root token block (accents, ink, type scale, gutters, the four beats)
 and the placement of its opening figure.

 Load order is a contract: screens.css → page.css → the page's <style>.
 Several rules here (`.sec-open`, `.sec-close`) share specificity with
 screens.css's `.sec` and win only because this file loads after it.

 Extracted from 0815.html so the
 Redis and Miscellaneous pages inherit the anatomy instead of copying it.
 Every accent follows var(--p)/var(--s) — never a literal — so the live
 colour-picker rule holds; translucent accents use
 color-mix(in srgb, var(--s) N%, transparent), the sanctioned alpha form.

 Tokens this file consumes beyond screens.css's contract:
 --dark (rgb triplet) dark ink, for accent-ground screens (opening)
 --head --line heading ink, hairline
 --serif --sans --mono the three stacks
 --t-kicker … --t-mono-s the 11-step type scale
 --page-base the near-black the ground dissolves through
 (default #070d0e, same as screens.css's fallback)
 Grid-count knobs (a page overrides these instead of rewriting the rules):
 --open-cards-n (3) --cards-n (3) --others-n (2)

 The ?v= on this file's <link> is deliberate and permanent — bump it in
 every consuming page whenever this file changes (see 0815.html's note).
 ═══════════════════════════════════════════════════════════════════════════ */

/* ── base resets + ground ────────────────────────────────────────────────── */
* { box-sizing: border-box; }
/* The document itself never scrolls — each screen is fixed and scrolls inside
   itself, so there is nothing here to scroll and a stray document scrollbar
   would only fight the wheel handler. --page-base is the near-black the
   ground dissolves through between screens. */
body { margin:0; background:var(--page-base,#070d0e); color:rgb(var(--ink));
       font-family:var(--sans);
       overflow:hidden; -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility; }
::selection { background:var(--p); color:#0a0c14; }
img { display:block; max-width:100%; }
h1,h2,h3 { margin:0; }
p { margin:0; }
a { color:inherit; text-decoration:none; }

/* Ground, not content: the wash and the hairlines belong to the screen's surface and
   must not travel with the words. `position:absolute` put them in the scroller's
   coordinate space — one viewport tall, anchored to the top of the scrolled content —
   so on any screen taller than the window the wash slid off the top and left the last
   stretch of reading sitting on flat --page-bg. `fixed` takes them out of the scroll
   entirely: they stay welded to the window while the copy moves over them, which is
   what "a fixed ground" meant in the first place.
   Note this only holds while nothing transforms the `.sec` — a transformed ancestor
   would become their containing block and hand the scroll straight back. That is why
   the over-scroll guard shifts the screen's CONTENT rather than the screen; see
   setShift in screens.js. */
.sec > .wash, .sec > .rules { position:fixed; inset:0; pointer-events:none; }
/* 12 hairlines, the design's ground texture — the one piece of Solaris chrome
   that survives onto a reading page. */
.rules { background-image:linear-gradient(90deg, rgba(var(--ink),.045) 1px, transparent 1px);
         background-size:calc(100% / 12) 100%; opacity:.5; }

.body { position:relative; z-index:6; padding-left:var(--gut-l); padding-right:var(--gut-r);
        display:flex; flex-direction:column; gap:clamp(26px,3.4vh,40px); }
.body.wide-gap { gap:clamp(30px,4vh,46px); }

/* ── the opening screen, on accent ground ────────────────────────────────── */
/* Classes, not ids: 0815 authored these as #s1/#s8, which worked while there was
   one page — a kit page marks its opening `sec sec-open` and closing `sec sec-close`
   whatever their ids are. Same declarations, one specificity step down (the load
   order above is what keeps them winning against screens.css's .sec). */
.sec-open { padding-bottom:clamp(40px,5vh,64px); display:flex; flex-direction:column; }
.sec-open .body { flex:1; gap:clamp(26px,3.4vh,44px); }

/* The nature of an ASCII figure stage — any stage, any figure. PLACEMENT and
   typography (position, size, cell, ink) stay per-page beside the recipe that
   drives them: see 0815.html's #f15-stage block for the worked example. */
.open-stage { margin:0; white-space:pre; overflow:hidden;
              user-select:none; pointer-events:none; }

/* 46vw, not the design's 52vw: the copy has to stop clear of a figure box on the
   right. At 52vw it ran under the jet at every width the stage is visible at. */
.open-col { max-width:min(830px,46vw); display:flex; flex-direction:column; gap:clamp(16px,2vh,24px); }
.open-kicker { font-family:var(--mono); font-size:var(--t-kicker); letter-spacing:.22em;
               text-transform:uppercase; color:rgba(var(--dark),.7); }
.open-h1 { font-family:var(--serif); font-weight:200; font-size:var(--t-h1); line-height:1;
           letter-spacing:-.025em; color:rgb(var(--dark)); text-wrap:pretty; }
.open-p { font-family:var(--sans); font-weight:300; font-size:clamp(17px,1.22vw,23px);
          line-height:1.55; color:rgba(var(--dark),.82); text-wrap:pretty; }
.open-p strong { font-weight:600; color:rgb(var(--dark)); }
.roles { display:flex; flex-direction:column; gap:5px; font-family:var(--mono);
         font-size:clamp(12px,.88vw,15px); color:rgba(var(--dark),.62);
         border-top:1px solid rgba(var(--dark),.3); padding-top:13px; }
.roles .now { color:rgb(var(--dark)); }
.roles .when { color:rgba(var(--dark),.45); }

.open-foot { max-width:min(830px,46vw); margin-top:auto; display:flex; flex-direction:column;
             gap:clamp(16px,2vh,22px); }
.open-cards { display:grid; grid-template-columns:repeat(var(--open-cards-n,3),1fr); gap:clamp(14px,1.4vw,22px); }
.open-card { display:flex; flex-direction:column; gap:12px; }
.open-card .shot { width:100%; aspect-ratio:16/10; background-size:cover;
                   background-position:center; outline:1px solid rgba(var(--dark),.3); }
.open-card .txt { display:flex; flex-direction:column; gap:6px;
                  border-top:2px solid rgba(var(--dark),.5); padding-top:11px;
                  transition:border-color .2s; }
.open-card:hover .txt { border-top-color:rgb(var(--dark)); }
.open-card .name { font-family:var(--serif); font-weight:400; font-size:clamp(21px,1.6vw,30px);
                   line-height:1; color:rgb(var(--dark)); }
.open-card .say { font-family:var(--sans); font-weight:300; font-size:clamp(13px,.94vw,16px);
                  line-height:1.45; color:rgba(var(--dark),.75); text-wrap:pretty; }
.open-scroll { display:flex; align-items:center; gap:12px; font-family:var(--mono);
               font-size:clamp(12px,.88vw,15px); color:rgba(var(--dark),.62); }
.open-scroll span:first-child { color:rgb(var(--dark)); }

/* ── section header ──────────────────────────────────────────────────────── */
/* One line: the title and its meta share the headline's row rather than the meta
   dropping beneath it. It used to wrap — the design drew this on a canvas wide enough
   that title + 400px of meta always fitted, and below about 1200px they stop fitting,
   leaving a band of empty space beside the title and the meta stranded under it.
   Nothing wraps now; both sides shrink instead, and the meta re-wraps internally. */
.head { display:flex; align-items:flex-end; justify-content:space-between;
        gap:clamp(24px,3vw,64px); flex-wrap:nowrap; }
.head-l { display:flex; flex-direction:column; gap:12px; min-width:0; flex:1 1 auto; }
.kicker { font-family:var(--mono); font-size:var(--t-kicker); letter-spacing:.22em;
          text-transform:uppercase; color:rgba(var(--ink),.6); }
.h2 { font-family:var(--serif); font-weight:200; font-size:var(--t-h2); line-height:.94;
      letter-spacing:-.025em; color:var(--head); }
.h2.small { font-size:var(--t-h2s); line-height:.98; text-wrap:pretty; }
/* flex:0 1 auto with a percentage cap — it gives way to the title rather than holding
   400px and forcing the row apart, and never takes more than a third of it. */
.meta-r { flex:0 1 auto; min-width:0; max-width:min(400px,34%);
          font-family:var(--sans); font-weight:300; font-size:var(--t-small); line-height:1.5;
          color:rgba(var(--ink),.66); margin-left:auto;
          text-align:right; text-wrap:pretty; }

.lede { display:flex; flex-direction:column; gap:clamp(9px,1.3vh,15px); max-width:min(1100px,72%); }
.lede p { font-family:var(--sans); font-weight:300; font-size:var(--t-lede); line-height:1.52;
          letter-spacing:-.005em; color:rgba(var(--ink),.9); text-wrap:pretty; }

/* ── case-study tabs ─────────────────────────────────────────────────────── */
/* position:relative so .tabs-ind — the sliding underline — has something to measure its
   left/width against. Built and driven entirely by screens.js's initTabs; see its own
   header comment for the choreography this and .panel's rules below exist to support. */
.tabs { position:relative; display:flex; gap:clamp(20px,2.2vw,38px); flex-wrap:wrap;
        border-bottom:1px solid var(--line); }
.tabs button { all:unset; cursor:pointer; padding:0 0 13px; display:flex; align-items:baseline;
               gap:9px; border-bottom:2px solid transparent; margin-bottom:-1px;
               transition:color .18s; }
/* The sweep is scoped down to the two characters of the number next to the
   title. So .num is the run of individually-addressable characters now — sweepNum in screens.js
 sets each digit's content and class on its own to run the wipe across them — and.lab
 went back to being one plain text node with the ordinary color fade it always had,
 below. inline-flex rather than the browser's default inline layout for.num so the two
 digits lay out exactly like plain text. */
.tabs .num { display:inline-flex; font-family:var(--mono); font-size:11px;
             letter-spacing:.14em; }
.tabs .ch { color:rgba(var(--ink),.34); transition:color .16s ease; }
/* .lit is the settled state a swept digit lands on — the accent, same as the old blanket
   [aria-selected] rule gave .num as a whole, now earned one digit at a time. .sweeping is
   the brief window a digit spends as one of the glyphs itself: white, so the wipe reads as
   a spark passing through on its way to the accent rather than the accent twice over. */
.tabs .ch.lit { color:var(--s); }
.tabs .ch.sweeping { color:var(--head); }
.tabs .lab { font-family:var(--sans); font-weight:400; font-size:clamp(16px,1.14vw,21px);
             letter-spacing:-.005em; color:rgba(var(--ink),.55); transition:color .18s; }
.tabs button:hover .lab { color:rgba(var(--ink),.8); }
.tabs button[aria-selected="true"] .lab { color:var(--head); }
.tabs button:focus-visible { outline:2px solid var(--s); outline-offset:3px; }
/* The underline itself — one element, sliding, instead of a border segment per button.
   It used to carry bottom:-1px, lining it up with the bar's own 1px border-bottom. That is
   the right place for it on a bar with ONE line and the wrong place on every other, because
   the bar's bottom edge stops being the selected button's bottom edge the moment the buttons
   wrap. The vertical is now written by screens.js's placeIndicator alongside left and width
   — into the 2px transparent border-bottom each button already reserves — so it is a
   property of the chosen button rather than of the bar it happens to sit in.
   `top` joins the transition so a move between rows travels rather than teleports. */
.tabs-ind { position:absolute; left:0; top:0; height:2px; width:0; background:var(--s);
            transition:left .26s cubic-bezier(.4,0,.2,1), width .26s cubic-bezier(.4,0,.2,1),
                       top .26s cubic-bezier(.4,0,.2,1);
            pointer-events:none; }
/* A panel is a column: usually one gallery, sometimes a gallery and the cards that
   read off it. `display:flex` and `[hidden]` fight — the attribute has to win. */
.panel { display:flex; flex-direction:column; gap:clamp(18px,2.4vh,30px);
         opacity:1; transition:opacity 140ms ease; }
.panel[hidden] { display:none; }
.panel.is-leaving { opacity:0; }
/* Arriving content stages in LEFT TO RIGHT, each direct child behind the one before it —
   the sideways twin of .sec.is-staging's top-to-bottom stagger for a screen's own arrival.
   Named .tabpane-… rather than reusing .is-staging/.is-on: those already mean something on
   .sec (scoped there to .body's children specifically), and a bare `.is-staging > *` rule
   here would also catch .sec's OWN direct children (.wash, .rules) — never part of that
   stagger. The class lands on whichever element actually holds the left/right split —
   usually .pane, or the panel itself when there's no .pane (every gallery-only tab, where
   there is nothing to stagger but the one gallery). .tabpane-entering is the second class,
   added a frame later (initTabs forces the reflow between them) so the browser actually
   renders the opacity:0 "from" state instead of computing style once at the end of one
   task and skipping the transition entirely — the same fix stageIn needed for the same
   reason. */
.tabpane-staging > * { opacity:0; }
.tabpane-staging.tabpane-entering > * { opacity:1;
                                        transition:opacity 240ms ease var(--tab-stagger,0ms); }
@media (prefers-reduced-motion:reduce) {
  .tabs-ind { transition:none; }
  .panel { transition:none; }
}

/* ── case-study panel interior ───────────────────────────────────────────── */
.pane { display:flex; gap:clamp(30px,3.2vw,56px); align-items:flex-start; }
.copy { width:min(620px,40%); flex:none; display:flex; flex-direction:column; gap:20px; }
.copy.wide { width:min(620px,44%); }
.copy.slim { width:min(620px,38%); }
.h3 { font-family:var(--serif); font-weight:300; font-size:var(--t-h3); line-height:1.08;
      letter-spacing:-.02em; color:var(--head); text-wrap:pretty; }
.p { font-family:var(--sans); font-weight:300; font-size:var(--t-body); line-height:1.62;
     color:rgba(var(--ink),.82); text-wrap:pretty; }
.p + .p { margin-top:-6px; }
.p em { font-style:normal; font-weight:500; color:var(--head); }
.tag { font-family:var(--mono); font-size:11px; letter-spacing:.16em; text-transform:uppercase;
       color:rgba(var(--ink),.42); }
/* An illustration that belongs to a passage of text rather than to a set — no frame, no
   caption, no gallery chrome. Held well under the column width: at full width three icons
   become a poster, and they are a footnote to the sentence above them. */
.aside-img { width:min(300px,68%); height:auto; margin-top:4px; align-self:flex-start; }
.fill { flex:1; min-width:300px; display:flex; flex-direction:column; gap:clamp(14px,1.8vh,20px); }

/* numbered problem list */
.numlist { display:flex; flex-direction:column; }
.numrow { border-top:1px solid var(--line); padding:clamp(16px,2vh,24px) 0;
          display:flex; gap:clamp(18px,1.8vw,30px); align-items:baseline; }
.numrow:last-child { border-bottom:1px solid var(--line); }
.numrow .n { font-family:var(--mono); font-size:13px; color:var(--s); flex:0 0 30px; }
.numrow h4 { margin:0; font-family:var(--serif); font-weight:400; font-size:var(--t-h4);
             line-height:1.18; color:var(--head); }
.numrow p { font-family:var(--sans); font-weight:300; font-size:var(--t-small); line-height:1.58;
            color:rgba(var(--ink),.68); padding-top:6px; text-wrap:pretty; }

/* persona row */
.personas { flex:1; min-width:300px; display:grid;
            grid-template-columns:repeat(auto-fit,minmax(148px,1fr)); gap:clamp(18px,1.9vw,30px);
            align-items:start; }
.persona { display:flex; flex-direction:column; gap:12px; align-items:center; text-align:center; }
.persona .face { width:min(160px,84%); aspect-ratio:1; border-radius:50%; overflow:hidden;
                 outline:1px solid rgba(var(--ink),.2); outline-offset:6px;
                 background-size:cover; background-position:center; }
.persona:first-child .face { outline-color:color-mix(in srgb, var(--s) 50%, transparent); }
.persona .name { font-family:var(--serif); font-weight:400; font-size:clamp(18px,1.28vw,23px);
                 line-height:1.15; color:var(--head); padding-top:4px; }
.persona .say { font-family:var(--sans); font-weight:300; font-size:clamp(13px,.92vw,16px);
                line-height:1.5; color:rgba(var(--ink),.66); text-wrap:pretty; }
.persona .facts { font-family:var(--mono); font-size:clamp(10px,.72vw,12px); line-height:1.5;
                  letter-spacing:.03em; color:rgba(var(--ink),.4); }

/* The authored form of a gallery item. screens.js reads these <figure class="fig">
 children — their frame's url and ratio, their caption — and rebuilds the set as one
 large picture with its text beside it, so nothing here is ever displayed as written.
 Kept as markup because a caption belongs where it can be read and diffed, not in a
 JS array. The.figs grid rules that used to lay them out three-up are gone with the
 thumbnails. */
.fig { display:none; margin:0; }
.fig .frame { width:100%; background-color:#f4f4f2; background-size:cover;
              background-position:top center; outline:1px solid rgba(var(--ink),.18); }
.fig .frame.dark { background-color:#060b0c; background-position:center; }
/* The design outlines the first tile in Secondary — that came from the picker
   grammar, where it meant "currently shown". These grids show everything at once,
   so a highlighted first tile would promise a selection that isn't there. */
.fig figcaption { font-family:var(--mono); font-size:var(--t-mono-s); line-height:1.5;
                  color:rgba(var(--ink),.52); border-top:1px solid rgba(var(--ink),.12);
                  padding-top:8px; display:flex; gap:8px; }
.fig figcaption .n { color:rgba(var(--ink),.34); flex:none; }

/* ── closing ─────────────────────────────────────────────────────────────── */
.sec-close { padding-bottom:clamp(40px,5vh,64px); display:flex; flex-direction:column; }
.sec-close .body { flex:1; gap:clamp(24px,3vh,38px); }
.cards { display:grid; grid-template-columns:repeat(var(--cards-n,3),1fr); gap:clamp(14px,1.4vw,22px);
         align-items:stretch; }
.card { border-top:2px solid color-mix(in srgb, var(--s) 40%, transparent); background:rgba(var(--ink),.035);
        padding:clamp(20px,2.4vh,28px) clamp(18px,1.6vw,26px); display:flex;
        flex-direction:column; gap:14px; }
.card:first-child { border-top-color:var(--s); }
.card .idx { font-family:var(--mono); font-size:clamp(11px,.8vw,14px); letter-spacing:.16em;
             text-transform:uppercase; color:color-mix(in srgb, var(--s) 85%, transparent); }
.card:first-child .idx { color:var(--s); }
.card .t { font-family:var(--serif); font-weight:400; font-size:clamp(23px,1.8vw,33px);
           line-height:1.12; color:var(--head); text-wrap:pretty; }
.card .d { font-family:var(--sans); font-weight:300; font-size:var(--t-small); line-height:1.58;
           color:rgba(var(--ink),.74); text-wrap:pretty; }
.card .d + .d { margin-top:-4px; }

.other-hd { display:flex; align-items:baseline; justify-content:space-between; gap:24px;
            border-top:1px solid rgba(var(--ink),.18); padding-top:14px; font-family:var(--mono);
            font-size:clamp(11px,.86vw,15px); letter-spacing:.16em; text-transform:uppercase;
            color:rgba(var(--ink),.58); }
.others { display:grid; grid-template-columns:repeat(var(--others-n,2),1fr); gap:clamp(10px,1.1vw,16px); }
.other { position:relative; overflow:hidden; min-height:104px;
         outline:1px solid rgba(var(--ink),.18); background:rgba(var(--ink),.03);
         padding:14px 16px; display:flex; flex-direction:column; justify-content:space-between;
         gap:10px; transition:outline-color .2s, background-color .2s; }
.other:hover { outline-color:var(--s); background:rgba(var(--ink),.055); }
.other .t { position:relative; z-index:1; font-family:var(--sans); font-weight:400;
            font-size:clamp(16px,1.1vw,20px); color:var(--head); }
.other .d { position:relative; z-index:1; font-family:var(--mono); font-size:clamp(11px,.8vw,14px);
            line-height:1.45; color:rgba(var(--ink),.48); }
/* The "figure standing behind the card" grammar — an ASCII figure (a turntable, a
 starfield) dimmed behind a tile's title and description, which stay the thing
 being read (z-index 1 over 0). First used on 0815's closing tiles. */
.other-fig { position:absolute; inset:0; z-index:0; margin:0; overflow:hidden;
             pointer-events:none; font-family:var(--mono); line-height:1; white-space:pre;
             color:rgba(var(--ink),.5); opacity:.4; }
@media (prefers-reduced-motion:reduce) { .other-fig { opacity:.28; } }
.foot { margin-top:auto; display:flex; align-items:center; justify-content:space-between;
        gap:24px; flex-wrap:wrap; padding-top:clamp(16px,2vh,24px); }
.foot-btn { display:inline-flex; align-items:center; gap:10px; border:1px solid color-mix(in srgb, var(--s) 70%, transparent);
            background:color-mix(in srgb, var(--s) 10%, transparent); padding:11px 17px; font-family:var(--mono);
            font-size:clamp(12px,.88vw,15px); letter-spacing:.06em; color:var(--head);
            transition:background-color .2s; }
.foot-btn:hover { background:color-mix(in srgb, var(--s) 22%, transparent); }
.foot-btn span:first-child { color:var(--s); }
.foot-end { font-family:var(--mono); font-size:clamp(11px,.86vw,15px); color:rgba(var(--ink),.45); }
.foot-end a { color:var(--s); }
.foot-end a:hover { color:var(--head); }

/* ═══ responsive ═══════════════════════════════════════════════════════════
   The design ships no media queries — it was drawn on a wide canvas. Three
   breakpoints below, each undoing exactly one desktop assumption.            */

/* 1200px — a right-pinned opening figure needs the right half of a wide screen
   or it reads as noise. (0815's own script assumed the F-15 gone by ~1100px.) */
@media (max-width:1200px) {
  .open-stage { display:none; }
  .open-col, .open-foot { max-width:none; }
}

/* 1000px — the rail's tree no longer has a column to live in, so it collapses to
   the compact one-line form the design already provided for exactly this. */
@media (max-width:1000px) {
  /* :root:root, not :root — the page's own :root block declares the desktop gutters and
     loads AFTER this file, so at equal specificity the base value would win here even
     with the media query active (exactly what happened when this rule first moved out of
     the page: the mobile gutter collapse silently died). Doubling the selector wins on
     specificity instead of order, which no load order can undo. */
  :root:root { --gut-l: clamp(24px,4vw,44px); --gut-r: clamp(24px,4vw,44px); }
  .body { padding-top:52px; }   /* clear the collapsed rail (screens.css) */
  /* every split becomes a stack: copy first, evidence under it */
  .pane { flex-direction:column; }
  .copy, .copy.wide, .copy.slim { width:100%; }
  .fill, .personas { width:100%; min-width:0; }
  .aside-img { width:min(260px,50%); }
  .lede { max-width:none; }
  /* stacked below 1000px, so the header wraps again and the meta goes back to the left */
  .head { flex-wrap:wrap; }
  .meta-r { text-align:left; max-width:none; margin-left:0; flex:1 1 100%; }
  .open-cards { grid-template-columns:repeat(var(--open-cards-n,3),1fr); }
}

/* 700px — phone. Multi-column grids give up. */
@media (max-width:700px) {
  .open-cards { grid-template-columns:1fr; }
  .open-card .shot { aspect-ratio:16/9; }
  .cards, .others { grid-template-columns:1fr; }
  .personas { grid-template-columns:repeat(2,1fr); }
  /* ── the tab bar scrolls sideways instead of wrapping ──────────────────────
     Only here, at the phone breakpoint. Between 700 and 1000px the labels still fit on one
     line and the row is already right; the wrap only ever happened below ~650px, so this is
     scoped to where the problem is and desktop and tablet keep the bar they had.

     Every way of keeping all six topics ON the screen at once costs vertical space — a wrap
     costs 93px, an aligned grid costs 108px — and vertical space is the thing a phone has
     least of. One line that scrolls costs 52px and holds it however many topics a page has:
     Redis's longest bar has six and its shortest has three, and both are now one line tall.
     What it trades away is seeing every topic at a glance, which is why the tab the reader
     is ON is scrolled into view rather than left to luck (revealTab, screens.js).

     No scrollbar: it would sit on the same 2px the sliding underline occupies and the two
     would read as one broken element. The affordance is the next tab cut off at the edge,
     which is the same one every phone uses.

     overscroll-behavior-x:contain so a sideways flick that runs out of tabs stops there
     rather than chaining outward into the browser's own back gesture. */
  .tabs { flex-wrap:nowrap; overflow-x:auto; overscroll-behavior-x:contain;
          scrollbar-width:none; -ms-overflow-style:none; }
  .tabs::-webkit-scrollbar { display:none; }
  /* nowrap on its own does not produce a scroller, it produces a squeeze. `all:unset` on the
     button resets flex to its initial `0 1 auto`, so shrink is 1 — the buttons gave up their
     width instead of overflowing, the labels wrapped INSIDE them, and a bar meant to be one
     line 52px tall came out one line 83px tall. The buttons have to refuse to shrink before
     there is any overflow for overflow-x to scroll.
     white-space on the label as well, so a two-word topic cannot break even in the moment
     before the flex sizing settles. */
  .tabs button { flex:0 0 auto; }
  .tabs .lab { white-space:nowrap; }
  .head { align-items:flex-start; }
  .foot { flex-direction:column; align-items:flex-start; }
}

@media (prefers-reduced-motion:reduce) {
  * { transition:none !important; }
}
