/* ==========================================================================
   mobile.css — the narrow builds.

   THIS IS NOT THE DESKTOP LAYOUT MADE NARROW. The desktop page is a fixed
   viewport with a virtual scroll and nine pinned, scrubbed scenes. None of
   that is right on a phone: the compositions assume a wide canvas, and the
   virtual scroll fights the browser's own.

   So the phone gets a different build of the same narrative:

     native scroll     the document scrolls, because the platform is better at
                       it than we are
     stacked scenes    one column, in reading order, auto height
     no scrubbing      the choreography that --p drives on desktop is replaced
                       by one entrance per element, from js/reveal.js
     everything else   the palette, the type scale, the captures, the boards,
                       the copy and the tone all survive

   LOADED LAST, and that is the point: every rule here overrides one in
   site.css at equal specificity, so load order is what makes the phone layout
   win. Deleting the <link> leaves the desktop site untouched.

   THE BREAKPOINT IS WRITTEN DOWN EXACTLY ONCE, in the 760px query below. A
   media query cannot take a custom property, so CSS owns it; the pre-paint
   switch and js/boot.js read --build instead of repeating the number.
   ========================================================================== */

/* ==========================================================================
   TABLET — same runtime, fewer columns and a shorter measure.
   ========================================================================== */
@media (max-width:1080px){
  .hero{ grid-template-columns:minmax(0,1fr); gap:var(--s-6); }
  /* The board reads at any width, unlike the capture this slot used to hold. */
  .hero__shot{ display:block; }
  .pboard{ width:min(100%, 20rem); margin:0 auto; }
  .pboard__read{ text-align:center; }
  .diff,.pipe,.local,.dlg{ grid-template-columns:minmax(0,1fr); gap:var(--s-6); }
  .pipe__feed{ height:34vh; }
  .rep__shot{ width:100%; }
  .rep__say{ position:static; max-width:none; margin-top:var(--s-5); }
  .rep__frags{ position:static; margin-bottom:var(--s-5);
               grid-auto-flow:column; justify-content:start; gap:var(--s-6); }
  .rail__tick b{ display:none; }
  .rail{ padding-right:12px; }
}

@media (max-width:900px){
  .prac__step{ position:static; width:100%; filter:none; transform:none; }
  .prac__word{ position:static; }
  .prac{ height:auto; display:grid; gap:var(--s-5); }
}

/* ==========================================================================
   THE PHONE BUILD
   ========================================================================== */
@media (max-width:760px){

  /* THE ONLY PLACE THE BREAKPOINT IS WRITTEN DOWN. The query above is the
     authority; this line is how JavaScript finds out what it decided. */
  :root{
    --build:mobile;
    --header-h:52px;
    --gutter:20px;
  }

  /* ---- give the document back to the browser --------------------------- */
  html,body{ height:auto; overflow:visible; overflow-x:hidden; }

  .stage{ position:static; inset:auto; overflow:visible; pointer-events:auto; }
  .track{ transform:none; will-change:auto; }

  .scene{
    /* js/scroll.js never mounts here, so no inline height was written and
       none needs undoing. A scene is a section again. */
    height:auto !important;
    content-visibility:visible !important;
  }
  .scene__pin{
    position:static; height:auto; display:block;
    transform:none !important; will-change:auto;
    padding-block:var(--s-8);
  }
  .scene:first-child .scene__pin{ padding-top:calc(var(--header-h) + var(--s-7)); }
  .scene__in,.scene__in--wide{ max-width:none; }

  /* THE SCRUB IS GONE, SO THE THINGS IT DROVE MUST BE ON.
     Every .at element computes its opacity from --p, which only the
     accumulator moves. Without this line the entire page is invisible on a
     phone — which is exactly the failure the desktop build's own reveal
     safety net exists to prevent, arriving from the other direction. */
  .at{ --a:1; }
  /* And the recede channel is pinned off. Nothing drives --p here, so --b
     would resolve from an undefined variable and take the whole filter
     declaration down with it; more to the point, the phone build stacks the
     captures one under another instead of overlapping them, so there is no
     depth to describe and nothing should be blurred. */
  .rc{ --b:0; }
  .at--out{ opacity:1; }
  .hero__say,.hero__shot,.diff__shot,.rep__shot{ transform:none; opacity:1; }


  /* ---- chrome ---------------------------------------------------------- */
  .hdr{ mix-blend-mode:normal; background:var(--canvas-deep);
        border-bottom:1px solid var(--hairline); }
  .nav{ display:none; }
  .burger{ display:inline-flex; }
  .rail{ display:none; }
  .hint{ display:none; }
  .marks{ display:none; }
  .strip{ display:none; }
  /* No corner marks down here, so nothing to register against: the control
     goes back to sizing from its own padding at the larger phone text size. */
  .hdr__actions .btn{ height:auto; padding:7px 13px; font-size:.78rem; }

  /* A menu that is open must not leave a scrollable page behind it. On
     <html>, not on body: iOS Safari ignores overflow:hidden on body when a
     scroll gesture is already in flight. */
  html.is-menu-open,html.is-menu-open body{ overflow:hidden; }

  /* ---- type ------------------------------------------------------------ */
  .t-mega{ font-size:clamp(2.1rem,11vw,3.2rem); }
  .t-big{ font-size:clamp(1.6rem,7.6vw,2.2rem); }
  .t-read,.t-note{ max-width:none; }

  /* ---- scene 02: the three states stack instead of overlaying ---------- */
  .prob__stage{ min-height:0; display:grid; gap:var(--s-7); }
  /* The cross-fade wrapper has no job down here - everything is already in
     flow - so it steps out of the box tree and lets its three children become
     grid items of .prob__stage directly. */
  .prob__over{ display:contents; }
  .prob__one,.prob__many,.prob__line,.prob__count{ position:static; inset:auto; }
  .prob__line > div{ align-items:flex-start; }
  .prob__gloss{ margin-inline:0; }
  .prob__one{ align-items:flex-start; text-align:left; }
  .prob__one .mvf{ justify-content:flex-start !important; }
  .prob__many{ grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px 16px; }
  /* Eighteen fragments is an accumulation on a wide screen and a wall on a
     phone. Six still reads as "this keeps happening". */
  /* +1 on every index: the column key is now the grid's first child. */
  .prob__many .mvf:nth-child(n+8){ display:none; }
  .prob__line{ justify-content:flex-start; text-align:left; }
  .prob__counters{ justify-content:flex-start; gap:var(--s-5); flex-wrap:wrap; }
  .met__sep{ display:none; }   /* wrapping turns a divider into a stray tick */

  /* ---- scene 03 -------------------------------------------------------- */
  .diff__engine{ padding-top:0; }

  /* ---- scene 04 -------------------------------------------------------- */
  .pipe__feed{ height:auto; mask-image:none; -webkit-mask-image:none; }
  /* The column cannot visibly thin without the scrub, so it simply shows
     what survives the filter rather than pretending to condense. */
  .pipe__rows .mvf:not(.keeps){ display:none; }
  .pipe__stages{ gap:var(--s-5); }

  /* ---- scene 05 -------------------------------------------------------- */
  .rep__shot{ width:100%; transform:none; }
  .rep__frags{ grid-auto-flow:row; gap:var(--s-3); margin-bottom:var(--s-5); }

  /* ---- scene 06 -------------------------------------------------------- */
  .prac{ gap:var(--s-6); }
  .prac__word{ font-size:1.1rem; margin-bottom:var(--s-2); }

  /* ---- scene 08 -------------------------------------------------------- */
  .dl__row{ grid-template-columns:auto minmax(0,1fr) auto; }
  .dl__m{ grid-column:2; grid-row:2; }

  /* ---- scene 09 -------------------------------------------------------- */
  .end__foot{ position:static; margin-top:var(--s-8); flex-direction:column;
              align-items:flex-start; gap:var(--s-3); }
  /* THE FOOTER SITS OUTSIDE .scene__in, SO IT HAS NO GUTTER OF ITS OWN.
     On desktop it is position:absolute with left/right set to var(--gutter),
     and that is where its inset comes from. The phone build makes it static
     to put it back in flow, which discards left/right - and because it is a
     sibling of .scene__in rather than a child, there is no padding-inline
     underneath to fall back on. Both lines ended up hard against the screen
     edge while everything above them was indented. */
  .end__foot{ padding-inline:var(--gutter); }
  .end__foot nav{ gap:var(--s-4); }

  /* ---- captures --------------------------------------------------------
     No bleeding and no tilt on a phone: a capture that runs off the edge of a
     390px screen is a capture nobody can read, and a rotation eats the width
     it needed. */
  .shot--bleed-r{ margin-right:0; }
  /* No zooming into a capture on a phone. .shot--crop scales the image up so
     a region fills a wide desktop frame; at 335px the frame is already
     narrower than the region, so the same zoom would show a corner of a
     panel and nothing legible. The hero is the only slot using it and it is
     display:none here anyway - this is so the next one that uses it does not
     have to rediscover why. */
  .shot--crop .shot__img{ transform:none; }
  .shot--tilt,.shot--tilt-r{ transform:none; }
  .shot--tall{ aspect-ratio:4 / 3; }

  .board{ --sq:min(34px,9.6vw); }

  /* ======================================================================
     THE EDITORIAL CUT — phone only.

     The phone build was a faithful transcription of the desktop page: same
     nine scenes, same copy, same nine captures, stacked. Faithful and far too
     long. It ran to 7,240px, nine full screens, 690 words and nine
     screenshots, and almost none of that survives a thumb: an eighteen-row
     accumulation grid whose whole point is an animation the phone does not
     run, a twenty-two-row engine feed doing the same, five stacked captures
     carrying thirty-six words between them.

     So the phone gets the edit, not the reduction. Nothing here shrinks type,
     tightens tracking or squeezes tap targets. It removes whole blocks that
     were carrying their weight on a wide screen and are carrying none here,
     and it keeps every heading, every headline number and the three captures
     that show something the copy cannot.

     TWO MECHANISMS, AND NEITHER CAN REACH THE DESKTOP BUILD:

       .m-cut    on the element, hidden here. Desktop has no rule for the
                 class, so adding it changes nothing there.
       .m-only   plus the `hidden` attribute in the markup. The attribute
                 hides it everywhere by default and this block reveals it,
                 which is how a phone gets its own shorter wording without a
                 single line of desktop CSS moving.

     A hidden capture also does not download. shots.js still builds every
     slot, but a lazy <img> inside a display:none box has no layout box, never
     intersects the viewport and is never fetched - so the six cut captures
     cost a phone nothing at all.
     ====================================================================== */
  .m-cut{ display:none !important; }
  .m-only{ display:block; }

  /* 02 - the accumulation grid. Eighteen fragments condensed to six still
     reads as a wall of notation, and the thing it exists to do - pile up as
     you travel - is a scrub the phone does not have. The scene still says
     one mistake, then the pattern, then the count. */
  .prob__many{ display:none; }

  /* 04 - the engine feed. Same reasoning: on desktop the column visibly
     thins as the filter runs. Here it was four surviving rows and a caption,
     stating an idea the five stages beside it already state. */
  .pipe__feed{ display:none; }
  /* And the five glosses. The stage NAMES are the pipeline - sync, evaluate,
     filter, explain, aggregate - and five paragraphs explaining five words is
     the repetition this edit is for. */
  .pipe__b{ display:none; }
  .pipe__stage{ padding-block:2px; }

  /* 03 - the patterns capture. It is the same screen scene 06 shows, and on
     a phone the two land four scroll-flicks apart with nothing between them
     but the argument they both illustrate. One of them is enough, and 06 is
     where it earns its place. Three captures survive on the whole page:
     the mistake list, the pattern report, the drill - analysis, aggregation,
     practice, which is the entire product in three pictures. */
  .diff__shot{ display:none; }

  /* 06 - two capture/word pairs instead of five, which is the section's own
     sentence: your weakness, then your drill.

     EACH PAIR GETS A BOX. Stacked in one column, a screenshot and the two
     words underneath it were four loose objects in a row with identical
     spacing between all of them, so nothing said which word belonged to
     which picture. A hairline around each pair says it once and says it
     structurally - the same card the application itself draws, at the same
     radius, off the same tokens. */
  .prac{ gap:var(--s-5); }
  .prac__cell{
    border:1px solid var(--hairline);
    border-radius:var(--r-card);
    background:var(--surface-1);
    padding:var(--s-3);
  }
  /* The capture carries its own hairline, which exists because a near-black
     screenshot on a near-black page has no edge of its own. Inside a box it
     has one, and two rules 12px apart read as a mistake. */
  .prac__cell .shot{ box-shadow:none; }
  .prac__cell .prac__step{ margin-bottom:0; }
  .prac__word{
    margin-bottom:0; margin-top:var(--s-3);
    /* The glow exists to lift the word off a capture it overlaps on desktop.
       In a box it sits on the box, and the shadow only muddies the type. */
    text-shadow:none;
  }
  /* The drills capture is a 16:10 window with an empty bottom third. Measured
     off the file, its content runs from 0.20 to 0.65 of the height and
     everything below that is flat background.

     cover scales to the frame's WIDTH here, so the board's size on screen is
     fixed by the box and cannot be improved by cropping - all a shorter frame
     does is stop printing the empty part. 2:1 at 34% was still showing
     0.07-0.87, which is most of the waste. 2.6:1 at 30% shows 0.12-0.73: the
     whole board, the whole prompt, and almost none of the background. */
  .prac__step--3 .shot{ aspect-ratio:2.6 / 1; }
  .prac__step--3 .shot__img{ object-position:50% 30%; }

  /* 02 - three grid children became two when the accumulation went, so the
     gap that separated three states is now holding two apart. */
  .prob__stage{ gap:var(--s-6); }

  /* Four counters, 307px of content in 335px of room: the flex row wrapped
     three-and-one, which turns a rising progression into a row of three and
     an orphan. Two-by-two keeps it reading as a progression - 1 to 11, 42 to
     97 - and gives the longest label ("times you made it") a column it fits
     in. Only this row: the pipeline's four labels are short and its single
     line is correct. */
  .prob__count .prob__counters{
    display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
    gap:var(--s-5) var(--s-4);
  }

  /* ---- the reading measure --------------------------------------------
     With the blocks gone the remaining prose is what the page is, so it gets
     a comfortable line rather than the full gutter-to-gutter width. */
  .t-read{ font-size:1.02rem; line-height:1.6; }

  /* Sections can breathe a little less now that each holds a third less.
     The RATIO of space to content is what reads as rhythm, and that is
     unchanged; this only stops nine near-empty scenes from adding a
     screen and a half of padding between them. */
  .scene__pin{ padding-block:var(--s-5); }

  /* ======================================================================
     SECOND PASS — still too much reading.

     The first cut removed the blocks that were doing nothing on a phone.
     What was left was still a wall: nine sections each opening with a label,
     a headline and a paragraph, and the paragraphs were written for a wide
     measure where twenty-four words are two lines. At 335px the same
     sentence is five, and five lines of body copy under every headline is
     what "lots of text" actually means.

     So this pass shortens the sentences rather than hiding more of them, and
     removes the two exhibits that were arguing a point the section above had
     already made. Every headline survives. Every number survives.
     ====================================================================== */

  /* 01 - the eyebrow. The masthead two centimetres above it already says
     PRAXIS, and "local-first game analysis" is the line the whole page is
     about to make at display size. */
  .hero__eyebrow{ display:none; }

  /* 03 - the engine readout. Six mono rows cut to three in the first pass,
     and three rows of notation under a label still occupy a fifth of the
     section to set up a contrast the phone cannot draw: on desktop the dim
     narrow column sits BESIDE the statement, which is the whole comparison.
     Stacked, it is just preamble. The statement is the section. */
  .diff__engine{ display:none; }

  /* 08 - the platform specs. "10 / 11 - x64 - zip" wraps to its own line
     under every row, four times. Which OS you are on is the choice; the
     build details are on the release page you are one tap from. */
  .dl__m{ display:none; }

  /* ---- captures: same three, less height ------------------------------
     A 4:3 frame of a dense analytics screen at 335px is 250px of texture.
     Cropping the frames wider keeps what makes each recognisable - the
     ranked cards, the board - and gives back most of a screen across the
     three of them. */
  .rep__shot .shot{ aspect-ratio:2 / 1; }
  .rep__shot .shot__img{ object-position:50% 34%; }
  .prac__step--2 .shot{ aspect-ratio:16 / 9; }

  /* ---- the measure ----------------------------------------------------
     The remaining paragraphs are short enough to hold a tighter leading
     without crowding, and the display type comes down a notch: 11vw put
     three-word headlines on three lines. Still the loudest thing on screen. */
  .t-read{ line-height:1.55; }

  /* Three claims on one flex line wrap to two, and a 26px row gap between
     them made the second look like a new block rather than the rest of the
     same row. The column gap stays wide; only the row gap closes. */
  .hero__meta{ gap:var(--s-2) var(--s-6); }
  /* Five stages that are now five single words do not need the leading of
     five paragraphs. */
  .pipe__stages{ gap:14px; }
  .end__foot{ margin-top:var(--s-6); }
  .t-mega{ font-size:clamp(1.95rem,9.6vw,2.9rem); }
  .t-big{ font-size:clamp(1.5rem,6.9vw,2rem); }


  /* ---- tap targets -----------------------------------------------------
     Cutting content is no good if what survives is hard to hit. Everything
     you can press gets a real 44px, which is the one number in this file
     that is about fingers rather than about reading. The footer links were
     14px tall - a text baseline, not a target. */
  .btn{ min-height:44px; }
  .hdr__actions .btn{ min-height:40px; }
  .burger{ min-width:44px; min-height:44px; }
  .end__foot nav{ gap:var(--s-2); flex-wrap:wrap; }
  /* No leading padding: the first link has to start on the same vertical as
     the line above it, and 2px of inset in a mono uppercase footer reads as a
     misalignment. The tap target grows to the right instead. */
  .end__foot nav a{ display:inline-flex; align-items:center;
                    min-height:44px; padding-inline:0 16px; }
  .dl__row{ min-height:56px; align-content:center; }
  .faq__q{ min-height:44px; display:flex; align-items:center; }


  /* ==========================================================================
     THE REVEAL — phone only.

     On the desktop build the scenes are choreographed against the scroll
     accumulator, which is a better version of this idea and would fight an
     IntersectionObserver doing it too. So js/reveal.js is in the MOBILE
     allowlist and nowhere else, and its styles live here for the same reason.

     The hidden state is gated on html.has-reveal, which the page head sets
     and js/boot.js withdraws if the module did not mount: a broken reveal
     must show everything, never nothing.
     ====================================================================== */
  html.has-reveal .rvl{
    opacity:0;
    transform:translateY(12px);
    transition:opacity 520ms var(--ease-out), transform 520ms var(--ease-out);
    transition-delay:calc(var(--i,0) * 60ms);
  }
  html.has-reveal .rvl.is-in{ opacity:1; transform:none; }
}

/* ==========================================================================
   ABOVE THE PHONE BUILD
   Anything that must be undone when the desktop layout is forced onto a
   narrow window with ?desktop lives here rather than as a default, so the two
   builds never depend on each other's leftovers.
   ========================================================================== */
@media (min-width:761px){
  .menu{ display:none !important; }
}

/* ==========================================================================
   ACTUAL SMALL PHONES
   ========================================================================== */
/* The display sizes that used to live here are gone. A separate scale below
   400px put most iPhones (375, 390) on it, and gave a 430px phone a SMALLER
   section headline than a 390px one. The phone build below sets one fluid
   scale across every width instead. */
@media (max-width:400px){
  :root{ --gutter:16px; }
  .brand span{ display:none; }
  .prob__many{ grid-template-columns:minmax(0,1fr); }
  .prob__many .mvf:nth-child(n+6){ display:none; }
  .prob__counters{ gap:var(--s-5); }
}

/* ==========================================================================
   THIRD PASS — designed for the phone, not cut down for it.

   The first two passes made the phone build shorter. This one makes it
   deliberate: the same tokens, the same voice, but each scene asks what a
   thumb can do with it. Placed after the small-phone tier on purpose, so one
   fluid scale runs from 320 to 760 with no step in the middle.

   Everything here is inside the 760px query, and everything it styles that
   the desktop build also has is reached only through phone-only classes
   (.m-only, .rule__n, .is-open, .lb...) or through rules that already lived
   in this block. Nothing here can reach a screen wider than 760px.
   ========================================================================== */
@media (max-width:760px){

  /* ---- the spacing ladder, frozen -------------------------------------
     site.css retunes these tokens on SHORT windows (820px and 700px tall),
     which is right for a pinned desktop scene that must fit one viewport
     and wrong for a phone that scrolls: a 375x667 phone was getting 16px
     section padding and smaller notes than a 390x844 one, by accident. The
     phone build has its own measure and holds it at every height: one
     notch tighter than the desktop ladder, at every height, so the page is
     consistent AND shorter rather than consistent and taller. */
  :root{ --s-5:20px; --s-6:28px; --s-7:40px; --s-8:56px; --s-9:80px; }
  .arch__row{ padding-block:10px; }
  .dl__row{ padding-block:12px; }
  /* 0, not a token: .prob__stage is a grid on the phone and its gap already
     separates the counters from the payoff. The margin doubled it. */
  .prob__count{ margin-top:0; }
  .prob__count-k{ margin-bottom:var(--s-4); padding-bottom:var(--s-3); }
  .prob__gloss{ margin-top:var(--s-4); }
  .hero__cta{ margin-top:var(--s-5); }
  .hero__meta{ margin-top:var(--s-4); }

  /* ---- one type scale, fluid end to end -------------------------------
     THE HERO HEADLINE IS THE LARGEST THING ON THE PAGE. A desktop rule
     (.hero .t-mega) out-specified the phone's own, so "Praxis finds the
     pattern" was set at 35px under 40px section headlines. It now leads
     every other headline at every width, and the engine's half sits a full
     step below it, which is the argument the two sizes exist to make. */
  .t-mega{ font-size:clamp(2.25rem, 10.4vw, 3rem); }
  .t-big,.diff .t-big{ font-size:clamp(1.6rem, 7.6vw, 2.05rem); }
  .hero .t-mega{ font-size:clamp(2.5rem, 12.8vw, 3.5rem); line-height:.86; }
  .hero .t-big{ font-size:clamp(1.3rem, 6.2vw, 1.7rem); }
  .t-read{ font-size:1.02rem; line-height:1.55; }
  .t-note{ font-size:.9rem; line-height:1.55; }
  .met__v{ font-size:clamp(1.6rem, 7.4vw, 1.9rem); }

  /* ---- the small labels, made legible ----------------------------------
     Section labels, metric labels, link affordances and the footer were 9px
     in --text-tertiary, 3.35:1 against the canvas. They carry real words -
     "games read", "times you made it", "demo data" - so on a phone they go
     to 11px in --text-secondary (6.9:1). The // separators stay tertiary:
     they are punctuation, not content. */
  .rule,.met__l,.prob__count-k,.rep__frag span,.end__foot,.micro-label,
  .dl__go,.arch__p,.prob__key{
    font-size:11px; letter-spacing:.12em; color:var(--text-secondary);
  }
  .arch__k,.dl__i,.pipe__n{ font-size:11px; letter-spacing:.12em; }
  .met__l{ line-height:1.35; }

  /* The numbers in section labels count the desktop rail, which the phone
     does not have; down here they read as gaps (02, 04, 06...). The menu
     numbers its own list instead. */
  .rule__n{ display:none; }
  .rule .m-only{ display:inline; }

  /* ---- masthead --------------------------------------------------------- */
  .brand{ min-height:44px; min-width:44px; }
  /* Drawn at the logo's and the burger's scale, so the masthead reads as one
     row of small marks rather than a button with an icon either side. The
     finger still gets 44px: the ::after extends the hit area past the box. */
  .hdr__actions .btn{
    position:relative; min-height:0; height:24px; padding:0 10px;
    font-size:10px;
  }
  .hdr__actions .btn::after{ content:''; position:absolute; inset:-10px -4px; }
  /* One solid control per screen. Over the hero, whose own primary is right
     there, the masthead's is hidden; js/phone.js sets .is-past-hero once the
     second section reaches the masthead, and the masthead's comes into focus
     out of a blur. It keeps its box while hidden, so the burger never shifts. */
  .hdr__actions .btn--solid{
    transition:opacity .35s ease, filter .35s ease, transform .35s ease,
               visibility .35s, background var(--dur-fast), border-color var(--dur-fast);
  }
  html.is-mobile:not(.is-past-hero) .hdr__actions .btn--solid{
    opacity:0; visibility:hidden; pointer-events:none;
    filter:blur(6px); transform:scale(.94);
  }
  @media (prefers-reduced-motion:reduce){
    html.is-mobile:not(.is-past-hero) .hdr__actions .btn--solid{ filter:none; transform:none; }
  }
  /* Reading progress, the phone's version of the desktop strip's bar. */
  .hdr__bar{
    position:absolute; left:0; right:0; bottom:-1px; height:1px;
    background:var(--orchid); opacity:.8;
    transform:scaleX(0); transform-origin:0 50%; pointer-events:none;
  }

  /* ---- the menu ---------------------------------------------------------
     IT OPENS UNDER THE MASTHEAD, not over it. Covering the header covered
     the only control that could close it; now the burger stays put and turns
     into a cross. A tap on empty space closes it too (js/nav.js). */
  .menu{
    inset:var(--header-h) 0 0 0; z-index:calc(var(--z-header) - 1);
    padding:var(--s-3) var(--gutter) calc(var(--s-6) + env(safe-area-inset-bottom, 0px));
    overscroll-behavior:contain;
  }
  .menu:not([hidden]){ animation:menuIn 240ms var(--ease-out); }
  .menu__link{
    font-size:clamp(1.4rem, 6.8vw, 1.85rem); padding:11px 0;
    border-bottom:1px solid var(--hairline);
    transition:color var(--dur-fast);
  }
  .menu__link i{ font-size:11px; letter-spacing:.12em; color:var(--text-secondary); }
  .menu__link[aria-current]{ color:var(--orchid); }
  .menu__link[aria-current] i{ color:var(--orchid); }
  .menu__foot{ margin-top:var(--s-6); }

  /* ---- 01 hero --------------------------------------------------------- */
  #hero .scene__pin{
    padding-top:calc(var(--header-h) + var(--s-6));
    /* The board lags the scroll (js/phone.js). Clipping here is what lets it
       sink behind the next section instead of sliding over its label. */
    overflow:hidden; overflow:clip;
  }
  .hero{ gap:var(--s-6); }
  .hero__cta{ gap:var(--s-3); }
  .hero__cta .m-only{ display:contents; }
  .hero__meta{ gap:var(--s-2) var(--s-5); }
  /* Smaller, so the first screen holds the claim, both buttons and the
     board; and a step lighter on its light squares, because at 1.3:1 the
     grid disappeared on a phone held outdoors. Still dark on dark. */
  .pboard{
    width:min(74vw, 17.5rem);
    --pboard-light:#3A3431;
  }
  .hero__shot{ transform-origin:50% 0; }
  .pboard__read{ margin-top:var(--s-3); }

  /* ---- 02 the gap -------------------------------------------------------
     The payoff is something Praxis would actually write, so it is set as a
     quotation from the product - sentence case, a source line above it -
     rather than as a fourth all-caps headline in a row. Four lines of tight
     uppercase was the hardest sentence on the page to read. */
  .prob__src{ margin-bottom:var(--s-3); }
  /* The engine readout is one nowrap line, 338px of it, and at 320px it
     pushed the whole scene's grid track past the screen, where overflow-x
     clipped the right edge of every line in it. The track may not grow past
     the column, and the readout breaks between its fragments - never inside
     one - when a phone is too narrow to hold it. */
  .prob__stage{ grid-template-columns:minmax(0,1fr); }
  .prob__one .mvf{ flex-wrap:wrap; row-gap:4px; }
  .prob__line .t-big{
    text-transform:none; letter-spacing:-0.028em; line-height:1.06;
    font-size:clamp(1.45rem, 6.6vw, 1.85rem);
  }

  /* 03 joins 02. "An engine tells you that you were wrong. Rarely why." is
     the conclusion of the scene above it, not a scene of its own; a rule and
     a full section's padding between them split one thought in two. */
  #difference .scene__pin{ border-top:0; padding-top:var(--s-2); }

  /* ---- 04 how it works: six stages that open --------------------------- */
  .pipe__stages{ gap:0; border-top:1px solid var(--hairline); }
  .pipe__stage{ display:block; padding:0; border-bottom:1px solid var(--hairline); }
  .pipe__tog{
    display:grid; grid-template-columns:2.6em minmax(0,1fr) auto;
    align-items:baseline; width:100%; min-height:44px;
    padding:9px 0; text-align:left;
  }
  .pipe__tog::after{
    content:'+'; font-family:var(--font-mono); font-size:13px;
    color:var(--orchid); padding-left:var(--s-4);
  }
  .pipe__stage.is-open .pipe__tog::after{ content:'\2212'; }
  .pipe__stage.is-open .pipe__b{
    display:block; max-width:none; margin:-2px 0 0 2.6em; padding-bottom:14px;
    font-size:.9rem; line-height:1.55;
    animation:stageIn 260ms var(--ease-out);
  }
  .pipe .prob__counters{
    display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
    gap:var(--s-5) var(--s-4);
  }

  /* ---- 05 the report ---------------------------------------------------
     The three counts were a column of three; as a row they read as one
     tally, the way the report itself lays them out, and give back 70px. */
  .rep__frags{
    grid-auto-flow:row; grid-template-columns:repeat(3,minmax(0,1fr));
    gap:var(--s-3); margin:var(--s-4) 0 var(--s-5);
  }
  .rep__frag{ flex-direction:column; align-items:flex-start; gap:6px; }
  .rep__frag b{ font-size:clamp(1.5rem, 7vw, 1.85rem); line-height:.9; }
  .rep__frag span{ line-height:1.3; }

  /* ---- 06 evidence and 08 local: spec rows ------------------------------
     Three columns at 340px squeezed the value into a wrapped middle column
     between a key and a status. A spec sheet on a phone reads key and status
     on one line, the value under them at full width. */
  .arch__row{ grid-template-columns:minmax(0,1fr) auto; gap:4px var(--s-4); }
  .arch__k{ grid-column:1; grid-row:1; }
  .arch__p{ grid-column:2; grid-row:1; text-align:right; }
  .arch__v{ grid-column:1 / -1; grid-row:2; font-size:.9rem; line-height:1.45; }

  /* ---- 07 practice ----------------------------------------------------- */
  .prac__note{ margin-top:var(--s-3); }

  /* ---- 09 research -----------------------------------------------------
     The three figures used to fall 1-then-2 because one label was long.
     Now the comparison is the layout: graph and board-only side by side,
     the pass rate under them, and each figure draws its own bar so 99.7
     against 34.8 is seen before it is read. Solid fills, no gradients: the
     linear-gradient() below is only how CSS spells a sized solid block. */
  #research .prob__counters{
    display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
    gap:var(--s-5) var(--s-4);
  }
  #research .met:nth-child(3){ grid-column:1 / -1; }
  #research .met::after{
    content:''; display:block; height:2px; margin-top:6px;
    background:
      linear-gradient(var(--bar,var(--text-secondary)),var(--bar,var(--text-secondary)))
        0 0 / var(--w,0%) 100% no-repeat,
      var(--hairline);
    transition:background-size 900ms var(--ease-out) 180ms;
  }
  #research .met:nth-child(1){ --w:99.7%; --bar:var(--text-primary); }
  #research .met:nth-child(2){ --w:34.8%; }
  #research .met:nth-child(3){ --w:78%; --bar:var(--orchid); }
  html.has-reveal #research .met.rvl:not(.is-in)::after{ background-size:0 100%, auto; }

  /* ---- 10 get praxis ---------------------------------------------------- */
  .dlg{ gap:var(--s-5); }

  /* ---- 11 end ----------------------------------------------------------- */
  .end__row{ margin-top:var(--s-6); gap:var(--s-3); }

  /* ---- captures you can open --------------------------------------------
     js/phone.js makes each visible capture a button and adds this tag. Same
     shape as the app's badges: 2px corners, a hairline, mono caps. */
  .shot[role="button"]{ cursor:zoom-in; -webkit-tap-highlight-color:transparent; }
  .shot[role="button"]:active{ opacity:.82; }
  .shot__zoom{
    position:absolute; right:8px; bottom:8px; z-index:2; pointer-events:none;
    font-family:var(--font-mono); font-size:10px; letter-spacing:.12em;
    text-transform:uppercase; color:var(--text-primary);
    padding:4px 7px; border-radius:var(--r-badge);
    background:rgba(12,11,10,.86); border:1px solid var(--hairline);
  }

  html.is-lb-open,html.is-lb-open body{ overflow:hidden; }
  .lb{
    position:fixed; inset:0; z-index:500;
    display:flex; flex-direction:column;
    background:var(--canvas-deep);
    padding-top:env(safe-area-inset-top, 0px);
    animation:menuIn 200ms var(--ease-out);
  }
  .lb[hidden]{ display:none; }
  .lb__bar{
    display:flex; align-items:center; gap:var(--s-2); flex:none;
    height:var(--header-h); padding:0 var(--gutter);
    border-bottom:1px solid var(--hairline);
  }
  .lb__cap{
    flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
    font-family:var(--font-mono); font-size:11px; letter-spacing:.12em;
    text-transform:uppercase; color:var(--text-secondary);
  }
  .lb__btn{
    min-height:44px; min-width:44px; padding:0 14px;
    font-family:var(--font-mono); font-size:11px; font-weight:700;
    letter-spacing:.12em; text-transform:uppercase;
    border:1px solid var(--hairline); border-radius:var(--r-btn);
    color:var(--text-primary);
  }
  .lb__btn[data-lb-close]{ border-color:var(--hairline-lit); color:var(--orchid); }
  .lb__pane{
    flex:1 1 auto; min-height:0; overflow:auto;
    overscroll-behavior:contain; touch-action:pan-x pan-y pinch-zoom;
  }
  /* Close to the capture's own size: 1600 of its 1920px, which puts the
     app's 13px interface text at about 11px - readable, and still a screen
     you can find your way around. Fit shows the whole frame at once. */
  .lb__img{ width:1600px; max-width:none; height:auto; }
  .lb.is-fit .lb__pane{ display:flex; align-items:center; }
  .lb.is-fit .lb__img{ width:100%; }
  .lb__hint{
    flex:none; padding:12px var(--gutter) calc(12px + env(safe-area-inset-bottom, 0px));
    border-top:1px solid var(--hairline);
    font-family:var(--font-mono); font-size:11px; letter-spacing:.12em;
    text-transform:uppercase; color:var(--text-secondary);
  }

  /* ---- reveal timing ----------------------------------------------------
     The reveal's stagger is per arrival batch now (js/reveal.js), so the
     step can be a little longer without anything waiting on its own. */
  html.has-reveal .rvl{ transition-delay:calc(var(--i,0) * 70ms); }
}

/* 09 - Install, phone build. The desktop timeline is cut (it is driven by
   the accumulator, which a phone does not have, and nobody installs Docker
   from a phone). Four steps and one stacked bar of where the download goes,
   all static: no module, so nothing to add to the MOBILE allowlist. */
@media (max-width:760px){
  /* A stepper: a numbered disc per step, joined by a hairline down the
     left, so five lines read as one sequence rather than five table rows. */
  ol.ins-m{ list-style:none; margin:var(--s-5) 0 0; padding:0; }
  .ins-m > li{
    position:relative; display:grid; grid-template-columns:24px minmax(0,1fr);
    gap:var(--s-3); padding-bottom:var(--s-5);
  }
  .ins-m > li:last-child{ padding-bottom:0; }
  .ins-m > li:not(:last-child)::before{
    content:""; position:absolute; left:11.5px; top:28px; bottom:4px;
    width:1px; background:var(--hairline-lit);
  }
  .ins-m b{
    display:grid; place-items:center; width:24px; height:24px; border-radius:50%;
    border:1px solid var(--orchid); color:var(--orchid);
    font-family:var(--font-mono); font-size:11px; font-weight:400; line-height:1;
  }
  .ins-m__t{ font-size:var(--t-small); color:var(--text-primary); line-height:24px; margin:0; }
  .ins-m__d{ font-size:12.5px; color:var(--text-secondary); line-height:1.5; margin:2px 0 0; }
  .ins-m .mono{ font-family:var(--font-mono); font-size:11px; }
  .ins-m__cmd{
    display:block; margin-top:var(--s-2); padding:10px 12px;
    font-family:var(--font-mono); font-size:10.5px; line-height:1.5; color:var(--text-primary);
    background:var(--canvas-deep); border:1px solid var(--hairline); border-radius:var(--r-card);
    overflow-wrap:break-word;
  }
  /* A flag stays whole: Chrome will break "--build" after its dashes. */
  .ins-m__cmd .nw{ white-space:nowrap; }

  /* Step 3: the two model sets, side by side. */
  .ins-m__sets{ display:grid; grid-template-columns:1fr 1fr; gap:var(--s-2); margin:var(--s-2) 0 var(--s-2); }
  .ins-m__set{
    padding:10px; border:1px solid var(--hairline); border-radius:var(--r-card);
  }
  .ins-m__set.is-def{ border-color:var(--hairline-lit); background:var(--accent-wash); }
  .ins-m__k{
    margin:0; font-family:var(--font-mono); font-size:10px; letter-spacing:.1em;
    text-transform:uppercase; color:var(--text-primary);
  }
  .ins-m__k span{ display:block; margin-top:2px; font-size:9px; color:var(--orchid); } .ins-m__k span.is-alt{ color:var(--text-tertiary); }
  .ins-m__gb{ margin:4px 0 8px; font-size:1.15rem; font-weight:600; color:var(--text-primary); line-height:1.1; }
  .ins-m__bar{ display:flex; gap:2px; height:5px; }
  .ins-m__bar i{ border-radius:3px; background:var(--orchid); }
  .ins-m__bar i:nth-child(2){ opacity:.65; }
  .ins-m__bar i:nth-child(3){ opacity:.4; }
  .ins-m__bar i.is-gap{ background:var(--hairline); opacity:1; }
  .ins-m__set .ins-m__d{ margin-top:8px; font-size:11.5px; line-height:1.4; }
}

/* Short phones in landscape (667x375 and the like are still under 760). The
   first screen is a strip; the board would push the claim off it. */
@media (max-width:760px) and (max-height:480px){
  #hero .scene__pin{ padding-top:calc(var(--header-h) + var(--s-5)); }
  .pboard{ width:min(44vh, 13rem); }
  .menu__link{ padding:8px 0; font-size:1.35rem; }
}

@keyframes menuIn{ from{ opacity:0; transform:translateY(-6px); } }
@keyframes stageIn{ from{ opacity:0; transform:translateY(-4px); } }

@media (prefers-reduced-motion:reduce){
  html.has-reveal .rvl{ opacity:1; transform:none; transition:none; }
  #research .met::after{ transition:none; }
}


/* ==========================================================================
   VERTICAL RHYTHM - phone only.

   Taken from skimp.rabugotra.com, measured at 390x844 in mobile Chrome, where
   every section reads as one clear step: a small label, the headline tucked
   right under it, the description close to the headline, then real air
   before the content, and a generous gap before the next section. Praxis had
   the same parts in the same order but uneven steps between them (20, 28,
   20, 16, 12px) and only ~40px between sections, so sections ran into each
   other and a headline sat as far from its own description as from the next
   block.

   The steps, as Skimp spaces them:

       label        -> headline        12px   --rh-label
       headline     -> description     16px   --rh-desc     (also desc -> desc)
       description  -> content         32px   --rh-content  (list, table, metrics, buttons)
       statement    -> statement       48px   --rh-group    (two ideas inside one section)
       section      -> section        128px   --rh-sec x2   (64 below, hairline, 64 above)

   LITERAL PIXELS, NOT THE --s-* SCALE. The --s-* tokens are redefined by the
   desktop build's height tiers, which also match short phones, so a rhythm
   built on them would quietly tighten on an iPhone SE. These do not move.

   !important appears only where the markup carries an inline margin
   (style="margin-top:var(--s-5)") that this layer has to win against.
   Everything here sits inside the phone query; nothing reaches desktop.
   ========================================================================== */
@media (max-width:760px){
  :root{
    --rh-label:12px;
    --rh-desc:16px;
    --rh-content:32px;
    --rh-group:48px;
    --rh-sec:64px;
  }

  /* ---- between sections ------------------------------------------------ */
  .scene__pin{ padding-block:var(--rh-sec); }
  /* The hero keeps its own top (header plus a composed first screen); only
     its bottom joins the rhythm. */

  /* 02 + 03 stay one thought (see "03 joins 02" above): no section break
     between them, just the gap between two statements in one section. */
  #problem .scene__pin{ padding-bottom:var(--rh-group); }
  #difference .scene__pin{ padding-top:0; }

  /* ---- label -> headline ---------------------------------------------- */
  .rule{ margin-bottom:var(--rh-label) !important; }
  .prob__said{ margin-top:0 !important; }

  /* ---- headline -> description, description -> description ------------ */
  .t-read,.t-note{ margin-top:var(--rh-desc) !important; }
  .prob__one .mvf{ margin-top:0 !important; }   /* the readout is the headline's description; the flex gap is 16 */
  .end .t-big.m-only{ margin-top:var(--rh-desc) !important; }

  /* Descriptions read more easily with Skimp's leading. */
  .t-read{ line-height:1.62; }
  .t-note{ line-height:1.6; }

  /* ---- description -> content ------------------------------------------ */
  .hero__cta{ margin-top:var(--rh-content); }
  .hero__meta{ margin-top:20px; }               /* claims belong to the buttons above them */
  .local,.dlg{ gap:var(--rh-content); }         /* intro block -> its table or link list */
  #research .prob__counters{ margin-top:var(--rh-content) !important; }
  #research .prob__counters + .t-note{ margin-top:var(--rh-content) !important; }
  #download ol.ins-m{ margin-top:var(--rh-content); }
  .end__row{ margin-top:var(--rh-content); }

  /* ---- statement -> statement, inside one section ---------------------- */
  .hero{ gap:var(--rh-group); }                 /* claim and buttons -> the board */
  .prob__stage{ gap:var(--rh-group); }          /* the blunder -> the pattern -> the count */
  .rep__frags{ margin-bottom:0; }
  .rep__say{ margin-top:var(--rh-group); }      /* the evidence -> what it means */
  .end__foot{ margin-top:var(--rh-group); }

  /* ---- content -> its footnote ----------------------------------------- */
  .arch > .micro-label{ margin-top:var(--rh-desc) !important; }
  .prac__note{ margin-top:var(--rh-desc); }

  /* ---- list rows ---------------------------------------------------------
     Skimp's list items breathe: about 16px of padding and 8px between a
     title and its text. The spec rows were 10px and 4px, which made four
     rows read as one dense block. */
  .arch__row{ padding-block:14px; row-gap:6px; }
  .prac{ gap:24px; }

  /* ---- the research strip ------------------------------------------------
     Full width under the claims, like the buttons above it, and a tap
     target (44px) rather than a label. The news wraps to two lines on its
     own; the chip and the arrow stay put beside it. */
  .hero__new{
    display:flex; width:100%; min-height:44px;
    margin-top:var(--rh-desc);
    padding:6px 12px 6px 6px; gap:10px;
    line-height:1.45;
    color:var(--text-primary);    /* the small-label rule above greys every .micro-label */
  }
  .hero__new-txt{ flex:1; font-size:10px; letter-spacing:.08em; }
  /* The chip is a tag on the news, not a second headline: smaller than the
     text it introduces, so the eye lands on the words. */
  .hero__new-tag{
    padding:2px 6px; gap:4px;
    font-size:9px; letter-spacing:.1em;
  }
  .hero__new-tag::before{ width:4px; height:4px; }

  /* ---- the footer ----------------------------------------------------------
     Four links in a 2 x 2 grid instead of a wrapping row: the row left
     "Contact" alone on a third line, 44px below the rest, and read as
     unfinished. Links first, under a hairline, so the footer starts with
     the things you can tap; the credit closes the page. Every link keeps
     its 44px target, and the grid's columns line its words up. */
  .end__foot{ gap:var(--s-4); }
  .end__foot nav{
    order:-1; align-self:stretch;
    display:grid; grid-template-columns:1fr 1fr; gap:0 var(--s-4);
    border-top:1px solid var(--hairline); padding-top:var(--s-2);
  }
  .end__foot nav a{ padding-inline:0; }
  .end__foot p{ color:var(--text-tertiary); }
}
