/* =============================================================================
   MOBILE.CSS — the phone build of the console.

   WHY THIS FILE EXISTS AT ALL, AND WHY EVERY RULE IN IT IS INSIDE A MEDIA
   QUERY.

   The standing constraint on this site is: the phone can be redesigned freely,
   THE DESKTOP MUST NOT CHANGE. That has been broken twice before by editing a
   shared rule and only noticing at one width. Putting the whole mobile pass in
   one file whose every selector sits inside `max-width` / `pointer: coarse`
   makes the guarantee STRUCTURAL rather than a promise — a rule in here cannot
   reach a 1440px window because there is no code path to it.

   It is also loaded LAST, after sections.css, for the reason the tap-target
   block learned the hard way: a media query carries no extra specificity, it
   only comes later if you put it later. An override sheet that is not last is
   not an override.

   -----------------------------------------------------------------------------
   THE SIX RULES THIS PASS FOLLOWS (the site's own six still apply on top)

   M1. ONE LEFT EDGE. Every block in a channel starts on the same vertical line.
       A card that adds its own padding on top of the gutter moves the reading
       edge in and out down the page, and on a 390px screen that wobble is the
       most visible thing on it. Cards go full-bleed to the gutter instead.

   M2. A BLOCK IS A LIST OR IT IS A SIGN-OFF. Lists are left-aligned and read
       down an edge; sign-offs are centred and read as one object. Nothing is
       half of each — a centred label over a left-aligned list is the fault that
       makes a layout look unfinished.

   M3. A PRIMARY ACTION SPANS THE COLUMN. On a phone the CTA is the widest thing
       on the screen or it reads as a suggestion.

   M4. NOTHING RIDES THE EDGE. Type that runs to the screen boundary reads as
       clipped. The frame keeps its inset even for full-bleed devices.

   M5. NO RAGGED WRAP. A run of items that wraps into ragged lines becomes an
       explicit grid or a horizontal scroller. Wrapping is a fallback, not a
       layout.

   M6. VERTICAL AIR IS PROPORTIONAL, NOT INHERITED. Desktop rhythm scaled by vw
       lands too loose on a phone, where the screen is the unit and every
       120px of air is a seventh of the view.
============================================================================= */

/* -----------------------------------------------------------------------------
   THE BREAKPOINT AND THE MOBILE SCALE.

   899px, matching the footer's existing phone breakpoint, so the site has ONE
   phone/tablet boundary rather than a private one per component. 900 is the
   guard width verified unchanged.
----------------------------------------------------------------------------- */
@media (max-width: 899px) {
  :root {
    /* THE CHANNEL RHYTHM ON A PHONE, AND IT WENT UP.

       `--ch` is 60px at this width and is the top and bottom padding of every
       channel. It was cut to 46 on the first pass, when the page was 16 screens
       and every one of them was worth arguing about — and that was one cut too
       many. Two channels meeting means 46 + 46 = 92px between the last line of
       one and the head plate of the next, which sounds generous and is not:
       ~24px of it is eaten by the sticky head's own plate, and the channels
       alternate by a tone step so faint that on an OLED phone in daylight there
       is nothing left saying a boundary was crossed. The owner's read — cramped,
       and no clear difference between sections — is both halves of that.

       66px, so a boundary is a full thumb of nothing. With the two heaviest
       blocks on the page now gone from the phone ("What I actually build" and
       the second lookup table), the height is affordable in a way it was not
       when 46 was chosen: the page is still shorter than it was this morning.

       IT IS THE ONE NUMBER THE WHOLE PAGE KEYS OFF. Every channel's top and
       bottom padding, and the negative margin that pulls each sticky head up
       through it, are written in terms of it — so the rhythm cannot drift
       section by section, which is what "focus on the spacing of every section"
       has to mean if it is to hold. */
    --m-ch: 66px;
    /* One block gap, used between every stacked component in a channel, so the
       page has a single vertical beat instead of each section's own. */
    --m-gap: 26px;
  }

  main .ch { padding-block: var(--m-ch); }
  main .ch .ch__head { margin-top: calc(var(--m-ch) * -1); }

  /* ---------------------------------------------------------------------------
     ...AND THE BOUNDARY IS DRAWN, NOT JUST SPACED.

     The rack alternates `--ch-bg` per channel, which on a wide screen is plenty:
     the eye takes a full-width tone step across 1440px easily. Across 390px of
     phone the same step is a few percent of luminance over a narrow band, and on
     an OLED screen at low brightness it is not there at all.

     A hairline is the honest fix and it is already the site's own vocabulary —
     every head plate, every list row and every card edge on this page is a 1px
     `--c-line`. The channel boundary being the ONE edge on the page that was
     implied rather than drawn is what made crossing one feel like scrolling
     through an undifferentiated column.

     ON THE SECTION, NOT THE HEAD PLATE: the head is `position: sticky` and a
     border on it would detach and ride down the screen. `:first-of-type` is
     excluded because the first channel's top edge is the bottom of the opening
     screen, which is not a boundary between two channels and does not want a
     line drawn under the hero.
  --------------------------------------------------------------------------- */
  main .ch + .ch { border-top: 1px solid var(--c-line); }
  /* The statement and the footer close the rack the same way. `.say` is hidden
     on a phone, so this is the footer's own top edge in practice. */
  main + .foot,
  main > .say { border-top: 1px solid var(--c-line); }

  /* THE HEAD PLATE GETS MORE SEPARATION FROM THE FIRST LINE OF THE CHANNEL.

     The head is pulled up through ALL of `--m-ch`, so raising that number does
     nothing at the top of a channel — it only pays out at the BOTTOM of the one
     before. The space under the plate is this margin and nothing else, and its
     `clamp(24px, 2.8vw, 40px)` floors at 24 on every phone: the channel's title
     started 24px under the bar that names it while the same title has 40px of
     air on a desktop, on the screen with a sixth of the room.

     Both halves of "cramped" are here, and they are different rules: the space
     BETWEEN channels is `--m-ch` above, the space INSIDE the top of one is
     this. */
  main .ch .ch__head { margin-bottom: clamp(30px, 8vw, 40px); }
}

/* =============================================================================
   THE BAR — THE PHONE GETS THE BRAND SWAP BACK, WITH A THIRD STATE.

   The desktop bar has always shown the mark on the opening screen and the
   wordmark once you leave it. The phone was deliberately excluded: the previous
   pass reasoned that the bar is mark + two controls with ~230px of empty strip
   between them, so the mark is not competing with anything and swapping only
   costs the brand box a constant width.

   That reasoning was about CROWDING and it was right about crowding. It missed
   what the swap is actually for: a lone glyph is an identity only to someone who
   already knows whose site this is, and a phone visitor scrolling a long page
   has nothing else on screen telling them. So the swap comes back, at the
   owner's request — and the bar has the room, measured: the wordmark is ~120px
   against ~230px of free strip.

   THE THIRD STATE IS THE FOOTER, and it is the reason `.solid` could not do
   this alone. `.solid` means "you have left the top", and by that test the
   footer is just more scrolling. But the footer prints the name in ~30px display
   type in the middle of the screen, exactly as the hero does — so a nav wordmark
   there is the same duplication the mark exists to avoid at the top. `.at-foot`
   is set by an IntersectionObserver on `.foot` in app.js.

   MARK at the top and at the foot; WORDMARK everywhere between.

   Desktop is untouched: it keeps the two-state swap it has always had. The
   footer rule is deliberately NOT extended to it here — that would be changing
   the desktop bar, and the brief has never been that.
============================================================================= */
@media (max-width: 620px) {
  /* Undo the phone's mark-only lock (base.css) whenever the bar is between the
     two ends. Every selector here carries `.solid` plus `:not(.at-foot)`, which
     outranks the two-class rules it is overriding without any !important. */
  .js .nav.solid:not(.at-foot) .nav__brand { width: var(--w-word, max-content); }
  .js .nav.solid:not(.at-foot) .nav__word { opacity: 1; pointer-events: auto; }
  .js .nav.solid:not(.at-foot) .nav__mark { opacity: 0; pointer-events: none; }

  /* …and back to the mark once the footer is on screen. `.at-foot` only ever
     coexists with `.solid`, so this cannot fight the opening-screen state. */
  .js .nav.at-foot .nav__brand { width: var(--w-mark, 1.1em); }
  .js .nav.at-foot .nav__word { opacity: 0; pointer-events: none; }
  .js .nav.at-foot .nav__mark { opacity: 1; pointer-events: auto; }

  /* BIGGER, at the owner's request. `--t-md` puts the wordmark at 16.3px on a
     phone — a size chosen for a bar that also has to hold seven channel links,
     which this one does not: it is the brand and two 44px controls with ~230px
     of empty strip between them. At ~22px the word measures ~155px and still
     leaves ~100px of slack, verified against the nav's padding edge.

     ONLY the word is re-sized. `.nav__mark` takes its own `font-size: --t-md`
     and sizes its height in `em` off it, so a change here would silently rescale
     the `J` too — they are two separate marks that happen to share a token. */
  .nav__word { font-size: clamp(1.3rem, 5.6vw, 1.6rem); }
}

/* =============================================================================
   THE BAR AND THE SHEET
============================================================================= */
@media (max-width: 899px) {
  /* ---------------------------------------------------------------------------
     AIR AT THE TOP OF THE SHEET.

     Asked for as "some padding between top and the start, only if space
     allows" — and the "if" is the whole engineering problem, because this sheet
     is sized to fit without scrolling and any unconditional padding spends
     height it may not have. So it is bought with `min-height`, on the same
     principle as the existing tier system: tall phones can afford it, short
     ones keep every pixel.

     840px, and NOT the 780 the existing (smaller) allowance uses — that number
     was measured, not chosen. At 800 tall the sheet already sits within ~20px
     of its screen, so this larger allowance pushed a 320x800 into scrolling by
     19px. A menu that scrolls hides its last block behind a gesture nobody
     knows to make, which is the reason the sheet was compressed to begin with.
     Between 780 and 839 the base rule's smaller allowance still applies, so
     those phones keep the air they already had and gain none they cannot pay
     for.
  --------------------------------------------------------------------------- */
  @media (min-height: 840px) {
    .menu__list { margin-top: clamp(18px, 4.2vh, 40px); }
  }

  /* ---------------------------------------------------------------------------
     ...AND THEN THE SLACK IS SHARED INSTEAD OF ALL FALLING TO THE BOTTOM.

     `.menu__foot { margin-top: auto }` gave every spare pixel to one gap.
     Measured at 390×844 that was 114px of nothing between the icon bay and the
     four cities — the largest space in the sheet, sitting under its quietest
     line, while the seven channels started 107px from the top of a screen they
     were pinned to. The owner's read: put the space at the top first, and keep
     some at the bottom after the socials.

     TWO AUTO MARGINS IS THE WHOLE MECHANISM. In a flex column, free space is
     split equally between every `auto` margin in the line, so adding one above
     the list turns 114/0 into 57/57 with no number written anywhere. And it is
     SELF-DISABLING, which is what makes it safe on a sheet that must never
     scroll: an auto margin resolves to zero when there is no free space, so the
     short phones — the ones the two compression tiers in base.css exist for —
     are not given a single pixel they cannot pay for. No height query needed.

     It overrides the fixed `margin-top` above rather than replacing it, and that
     ordering is deliberate: the clamp is what tall phones fall back to if this
     rule is ever removed.
  --------------------------------------------------------------------------- */
  .menu__list { margin-top: auto; }

  /* AIR BEFORE "DIRECT", asked for directly. The sheet's tail is two labelled
     blocks under a seven-item list, and at 16px the first divider sat closer to
     the last channel than the channels sit to each other — so "Direct" read as
     an eighth row of the list rather than as the start of something else. The
     other tier rules in base.css set this at `--s-4` (16px) and `10px`; this is
     the tallest-phone tier and it can afford the separation that says the list
     has ended. */
  @media (min-height: 780px) {
    .menu__sec { margin-top: clamp(20px, 3.4vh, 34px); }
  }

  /* ---------------------------------------------------------------------------
     THE SHEET PUTS THE FULL NAME IN THE BAR.

     The phone bar shows the `J` mark at the top of the page and at the footer,
     and the wordmark in between (see the block above). Open the menu at the top
     of the page and you get the mark — a lone glyph over a full screen of
     navigation, which is the one moment the site is being ASKED who it belongs
     to. The menu is not "more scrolling"; it is a stop, and a stop is where a
     mark should say its name.

     It also costs nothing: the sheet has covered the page, so the wordmark is
     not competing with anything, and the burger is already full amber beside it.

     FIVE CLASSES, BECAUSE OF WHAT IT HAS TO BEAT. The state it overrides is
     `.js .nav.at-foot .nav__word` (four), which is live whenever the menu is
     opened from the footer — the commonest place anyone opens it on a long page.
     `body.menu-open` is a state on the body and `.js` is on the root, so the
     selector reads root → body → bar → mark and outranks both the at-foot rule
     and the base phone lock with no `!important` anywhere.
  --------------------------------------------------------------------------- */
  .js body.menu-open .nav .nav__brand,
  .js body.menu-open .nav.at-foot .nav__brand { width: var(--w-word, max-content); }
  .js body.menu-open .nav .nav__word,
  .js body.menu-open .nav.at-foot .nav__word { opacity: 1; pointer-events: auto; }
  .js body.menu-open .nav .nav__mark,
  .js body.menu-open .nav.at-foot .nav__mark { opacity: 0; pointer-events: none; }

  /* ---------------------------------------------------------------------------
     THE SWAP IS SEQUENCED, NOT SIMULTANEOUS — the mark clears before the word
     arrives.

     The two marks are stacked in ONE grid cell (`.nav__brand > * { grid-area:
     1/1 }`) with a clipped box that animates its width between them. Both were
     crossfading on the same `--d-2`, so for ~130ms in the middle there were two
     brand marks painted on top of each other at the same left edge — a `J` and
     the word "Jesvi" sharing the same pixels. On the scroll swap you rarely
     catch it; opening the menu fires it under your thumb with a full-screen
     sheet behind it, which is where the owner saw it.

     THE ORDERING COMES FROM WHICH STATE THE RULE IS IN, NOT FROM JS. A
     transition is taken from the rule that describes the state being entered —
     so the LEAVING glyph keeps the fast, undelayed base transition and only the
     ARRIVING one carries a delay. That gets both directions right (open AND
     close, scroll down AND back up) from one pair of rules, with nothing to keep
     in step.

     `body:not(.menu-open)` on the mark's arrival rule is load-bearing: at the
     top of the page `.nav:not(.solid)` is true while the menu is open, so
     without it the mark would take the 130ms delay on its way OUT and overlap
     the word it is supposed to be clearing for.

     The width is retimed to match. `--d-4` outlasted the crossfade, so the box
     was still growing after the word had finished arriving — which is what made
     the word look like it was being wiped in rather than faded. */
  .js .nav .nav__word {
    transition: background-position var(--d-5) var(--e-out), opacity 130ms linear;
  }
  .js .nav .nav__mark { transition: opacity 130ms linear, color var(--d-2); }

  .js .nav.solid:not(.at-foot) .nav__word,
  .js body.menu-open .nav .nav__word,
  .js body.menu-open .nav.at-foot .nav__word {
    transition: background-position var(--d-5) var(--e-out),
                opacity 200ms var(--e-out) 140ms;
  }
  .js body:not(.menu-open) .nav:not(.solid) .nav__mark,
  .js body:not(.menu-open) .nav.at-foot .nav__mark {
    transition: opacity 200ms var(--e-out) 140ms, color var(--d-2);
  }

  /* The other two are restated because this is a SHORTHAND: dropping them here
     would silently delete the brand's hover scale and its accent bloom. */
  .js .nav__brand {
    transition: width 300ms var(--e-out),
                transform var(--d-2) var(--e-spring),
                filter var(--d-3) var(--e-out);
  }

  /* ---------------------------------------------------------------------------
     BOTH SECTION LABELS IN THE SHEET ARE CENTRED.

     The centred version brackets the label with a rule on BOTH sides rather
     than centring the text and leaving one trailing rule, because a single rule
     is a direction and the point here is that nothing is being pointed at.
     `.ch__rule` is already in the markup; the mirror is a ::before, which is
     free — the only pseudo in use on this component is `.ch__rule::after`, and
     that is scoped to `.ch .ch__head`, which these are not.

     "Elsewhere" was centred first because its content is a centred icon bay.
     "Direct" was deliberately left ranged-left on the reasoning that a label
     follows its content and its content is a LIST — but the owner asked for
     both, and looking at it he is right: these two labels are the sheet's only
     dividers, they sit one above the other down the same tail, and matching
     them makes the tail read as one closing block. The rows underneath are
     still a left-read list; the divider above them is not part of it.
  --------------------------------------------------------------------------- */
  /* ---------------------------------------------------------------------------
     NO LEADING RULE ON EITHER LIST (asked for, of "01 Expertise" and the email
     row).

     `.menu__list` and `.menu__rows` each carry a `border-top` and every item in
     them carries a `border-bottom`, so the first row is the only one boxed on
     both edges. On a desktop-height sheet that top rule is the line the list
     hangs from; on a phone, where the list is the first thing under a bar that
     already has its own bottom edge, it lands ~14px under that edge and reads as
     a double rule — two hairlines with a sliver of background between them.

     Dropping it costs the list nothing: seven bottom borders still rule every
     row, and the last one closes the block. A list needs a line BETWEEN its
     items and a line UNDER it; the one above the first item is the one that was
     only ever separating it from something else.
  --------------------------------------------------------------------------- */
  .menu__list,
  .menu__rows { border-top: 0; }

  /* ---------------------------------------------------------------------------
     THE TAIL OF THE SHEET ARRIVES TOO — "Direct has no animation?"

     It did not, and the omission is visible precisely because the seven channels
     above it do: they play in on a 60→300ms stagger written into the markup as
     `--md`, so the eye follows a wave down the list and then hits two blocks
     that were simply already there. A sheet that assembles down to a point and
     then stops half way reads as an animation that broke, not as a design.

     CONTINUING THE SAME WAVE RATHER THAN INVENTING A SECOND ONE. The channels
     end at 260ms, so the two `.menu__sec` blocks and the sign-off take 330 / 400
     / 470 — the same 70ms step, the same 14px rise, the same easing. It is one
     movement that happens to cross three components.

     The delays are set HERE and not in the markup because these three are not a
     list: there is no index to write them against, and hard-coding a `--md` on a
     `<div>` in index.html would be a number the desktop carries for a phone-only
     effect. `:nth-of-type` is safe on them for the same reason it is unsafe on
     the channels — nothing is ever inserted between them.
  --------------------------------------------------------------------------- */
  .menu__sec,
  .menu__foot {
    opacity: 0;
    transform: translateY(14px);
    transition: opacity var(--d-3) var(--e-out), transform var(--d-3) var(--e-out);
  }
  body.menu-open .menu__sec,
  body.menu-open .menu__foot { opacity: 1; transform: none; }
  body.menu-open .menu__sec:nth-of-type(1) { transition-delay: 330ms; }
  body.menu-open .menu__sec:nth-of-type(2) { transition-delay: 400ms; }
  body.menu-open .menu__foot { transition-delay: 470ms; }

  /* A sheet that is opened with reduced motion asked for should not have three
     blocks fading in behind the seven that already do not. */
  @media (prefers-reduced-motion: reduce) {
    .menu__sec,
    .menu__foot { opacity: 1; transform: none; transition: none; }
  }

  .menu__sec .ch__head { justify-content: center; }
  .menu__sec .ch__head::before {
    content: '';
    flex: 1; height: 1px; min-width: 16px;
    background: var(--c-line);
  }

  /* ---------------------------------------------------------------------------
     THE CITIES ARE A SIGN-OFF, SO THEY HAVE TO LOOK CENTRED — and a line that
     fills its container edge to edge is not visibly centred even when it is.

     At 390 the four cities and their separators measure the full column, so
     `text-align: center` had nothing to centre with and the line sat against
     both edges, which reads as text that has run out of room (rule M4).

     Wrapping it to two lines fixes the edges and introduces a worse fault: the
     separators are part of the string, so any break point strands a dot at the
     end of the first line ("CHENNAI · BENGALURU ·"). The size gives way
     instead, exactly as in the footer, which carries the identical line — one
     line, air at both ends, and the original size restored from ~418px up.
  --------------------------------------------------------------------------- */
  .menu__foot { padding-top: var(--s-5); }
  .menu__foot .mono {
    display: block;
    font-size: clamp(8.6px, 2.72vw, 11.36px);
    line-height: 1.7;
    color: var(--c-faint);
  }
}

/* =============================================================================
   THE OPENING SCREEN
============================================================================= */
@media (max-width: 620px) {
  /* ---------------------------------------------------------------------------
     THE DISCIPLINE LEGEND: EVEN GAPS, NOT `space-between`.

     Measured at 390: the four labels are 237px of type inside a 354px row, and
     `space-between` spent all 117px of the remainder on three gaps of 39px —
     wider than "Engineer" is. That is what made the row read as four separate
     labels floating at the foot of the screen rather than as one legend, and
     it is what the reader was seeing when they said the tags "look weird".

     The words were never the problem; the SPACING BETWEEN THEM was. A legend is
     a run of type and a run of type has ONE rhythm — so the gap is a fixed
     value the whole row shares and the line simply ends where it ends. Ragged
     right is correct here: the row is a caption, not a justified column, and
     even gaps are what make four words read as one line (rule M5).

     Sized to hold one line from 320 through 430: at 320 the labels are 201px
     and three 14px gaps take it to 243 in a 268px column; at 430 they are 262px
     and three 21.5px gaps take it to 326 in 378. No wrap at any width in range.

     CENTRED, at the owner's request. The run is shorter than the column it sits
     in (295px of 338 at 390), so centring splits the remainder into two equal
     margins instead of leaving it all at the right — which is what makes the
     row read as a caption UNDER the composition rather than as a fourth line of
     it. The even gaps are what make centring work at all: with `space-between`
     there was no slack left to centre.
  --------------------------------------------------------------------------- */
  .hero__discs {
    justify-content: center;
    gap: clamp(14px, 5vw, 26px);
  }

  /* The legend's BOX still lines up with the name's column (rule M1) even though
     its content is now centred inside it. The legend takes `--gut-chrome` and
     the name takes `--gut`: on a desktop that is a deliberate 38px difference
     and reads as the frame the strip belongs to, but on a phone the two are 18
     and 26 — too close to read as a different inset, and exactly far enough
     apart to read as a mistake. Matching them means the centred run is centred
     on the same axis as the headline above it. */
  .hero__bottom { margin-inline: 0; }

  /* ---------------------------------------------------------------------------
     THE BLOCK SITS JUST BELOW CENTRE — NOT BOTTOM-ANCHORED, NOT DEAD CENTRE.

     History, because this row has now been set three ways and each one was a
     reaction to the last. It was `justify-content: flex-end` with a 93px pad,
     which left a 117px hole above the foot legend. Closing that hole to 44px
     removed the hole and revealed the real fault: with the stack pinned to the
     bottom of its row, the whole composition sat on the floor of the screen —
     the owner's words, "sticking to bottom".

     ANCHORING IS THE WRONG TOOL FOR A POSITION THAT IS MEANT TO BE A RATIO.
     `flex-end` plus a pad says "this far up from the bottom", so the block's
     position is whatever is left after the padding — it tracks the padding, not
     the screen. `center` plus a padding-top says "centred, then biased down by
     half of this", which is what was actually wanted: the free space is SHARED,
     roughly 60/40 instead of 88/12, and the bias survives a change of screen
     height because both halves scale with the same box.

     Centring is also the safer of the two on a short phone: a bottom anchor
     with a fixed pad pushes the headline up under the nav as the screen gets
     shorter, while a centred block simply has less room on both sides.

     Measured at 390x844: the headline lands at ~35% of the screen instead of
     48%, and the gap under the buttons goes 44 -> ~130px, which now reads as
     the lower half of a centred composition rather than as an omission —
     the difference being that the block above it no longer touches the floor.
  --------------------------------------------------------------------------- */
  .hero__mid {
    justify-content: center;
    /* The bias. Content moves down by HALF of this, so ~10.5vh buys ~44px of
       downward lean at 844 — present, but well short of an anchor. */
    padding-top: clamp(40px, 10.5vh, 104px);
    padding-bottom: 0;
  }

  /* ---------------------------------------------------------------------------
     ...AND THEN GIVE THE RECLAIMED HEIGHT BACK TO THE CONTENT, because closing
     the hole on its own MOVED THE NAME DOWN.

     The name is positioned from the BOTTOM of the screen, not the top — it is
     the last thing above a bottom-anchored stack — so every pixel taken out of
     the stack lowers it by the same pixel. Closing a 117px hole to 44px sent
     the headline from 46% down the screen to 54%, which is past the point where
     a viewer reads it as composed and starts reading it as fallen to the floor.

     So the block grows into the space instead of the void keeping it: a bigger
     name, buttons at a proper thumb height, and a more generous rhythm between
     the three. All three are things a phone wants anyway — this is the same
     diagnosis the first mobile pass made (the content was too small for the
     screen), applied one step further now that there is height to spend.
  --------------------------------------------------------------------------- */
  .hero__name { font-size: clamp(3.5rem, 17vw, 4.6rem); }
  .hero__pitch { margin-top: clamp(20px, 3.2vh, 30px); }
  .hero__cta { margin-top: clamp(28px, 4.4vh, 42px); gap: var(--s-3); }
  /* 54px, not the 44px floor. 44 is the smallest a target may be; the two
     primary actions on the opening screen are not the place to sit on the
     minimum, and a taller button is also what makes the pair read as the
     weight at the foot of the composition rather than as two thin strips. */
  .hero__cta .btn { min-height: 54px; }
}

/* =============================================================================
   CH 01 — EXPERTISE
============================================================================= */
@media (max-width: 899px) {
  /* ---------------------------------------------------------------------------
     THE FILTER BAR IS ONE ROW THAT SCROLLS, NOT THREE THAT WRAP.

     Measured at 390: the six buttons need 590px and the bar offers 302, so
     `flex-wrap` broke them into ragged rows — [All · Languages · Frontend] /
     [Backend · AI & ML] / [Cloud / DevOps] — 151px of bar for six controls, and
     a last row holding one button beside 150px of nothing. A wrapped control
     strip also has no shape: add a category and the rows re-break somewhere
     else.

     A grid was tried on paper first and does not survive the content. Two
     columns need 147px cells and "Cloud / DevOps" is 143px at 390 but 143 in a
     133px cell at 360, so the longest label wraps inside its own button on the
     second-commonest phone width. Three columns are worse. The labels are the
     content and they are not negotiable, so the LAYOUT gives way instead: one
     row, scrolled horizontally, which is the pattern every phone user already
     knows and the one that does not care how many categories there are.

     It also happens to be the truest to the section — a strip of channel
     buttons you slide along IS the console the site is drawn as.

     `min-width: 0` is load-bearing: a flex item will not shrink below its
     content width without it, so the strip would simply push the bar wide
     instead of scrolling inside it.
  --------------------------------------------------------------------------- */
  .js .kit__bar {
    flex-wrap: nowrap;
    gap: 0;
    padding-inline: 0;
  }
  .solo__set {
    flex-wrap: nowrap;
    min-width: 0;
    overflow-x: auto;
    /* Stops a sideways flick from being taken as a browser back-swipe. */
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    gap: 8px;
    /* The inset the buttons would have had from the bar's own padding, moved
       onto the scroller so the first and last button still line up with the
       panel edge while the row scrolls the full width underneath it. */
    padding-inline: 18px;
    /* Fades the two ends so a row that continues off-screen says so. The stops
       match the padding, so at rest — scrolled hard left — the fade covers only
       empty inset and the first button is at full strength. */
    -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 18px, #000 calc(100% - 18px), transparent 100%);
    mask-image: linear-gradient(90deg, transparent 0, #000 18px, #000 calc(100% - 18px), transparent 100%);
  }
  .solo__set::-webkit-scrollbar { display: none; }
  .solo { flex: 0 0 auto; }

  /* A SCROLL CONTAINER DOES NOT STOP ITS OWN MIN-CONTENT WIDTH PROPAGATING, AND
     THAT COST ME A ROUND — the strip scrolled correctly and the whole page went
     546px wide at a 390px viewport instead.

     `overflow-x: auto` plus `min-width: 0` makes the strip itself shrinkable,
     which is why it scrolls. Neither changes the fact that a `nowrap` flex
     container's MIN-CONTENT SIZE is still the sum of its items — 518px here —
     and `.ed__grid` and `.ed__body` are grid items, whose automatic minimum
     size (`min-width: auto`) is exactly that min-content size. So the track was
     floored at 518 by a button row four levels below it, every paragraph in the
     channel was laid out 520px wide, and the document grew to fit.

     `min-width: 0` on the grid items is the release valve. THE RULE WORTH
     REMEMBERING: putting a horizontal scroller inside a grid or flex layout
     needs `min-width: 0` on every item between it and the page, not just on the
     scroller — the scroller controls whether IT overflows, the ancestors
     control whether they are allowed to be narrower than it wants to be. */
  .ed__grid,
  .ed__body { min-width: 0; }

  /* The kicker ("Profile · Software engineering") was hidden here first and is
     now hidden EVERYWHERE at the owner's request — the rule moved to
     sections.css, which is the only file that can reach a desktop. Nothing to do
     here any more. */

  /* ---------------------------------------------------------------------------
     THE PULL QUOTE COMES OFF THE PHONE TOO (owner's request), and it is the same
     kind of cut as the one below it.

     "Anyone can demo. The engineering is in what still works at 3am, on a bad
     network, on the hundred-thousandth request" is a restatement of the
     paragraph immediately above it — which already says "assume it will be
     handed the worst input on the worst night, and design for that first". On a
     wide screen a pull quote is a change of pace across a measured column and
     costs nothing; in a single narrow column it is the same sentence said twice
     at two sizes, ~180px apart.
  --------------------------------------------------------------------------- */
  .ed__q { display: none; }

  /* ---------------------------------------------------------------------------
     "WHAT I ACTUALLY BUILD" COMES OFF THE PHONE (owner's request), AND IT IS THE
     RIGHT BLOCK TO LOSE.

     CH 01 opens by saying it: the deck is "I build the backend systems
     businesses run on, the web and app layer on top, and the AI tooling teams
     use every day". The Experience plate says it again with a system attached,
     the stack bank says it in 26 chips, and Selected work says it six times with
     links. This subsection is the one place the claim is made in the abstract,
     and it costs ~900px — a full screen — of a channel that was 4.5 of them.

     THE `.ed__seg--build` HOOK IS THREE SIBLINGS, NOT A WRAPPER. See the note in
     index.html: a wrapper would have changed the desktop's margin collapsing,
     and an unqueried class name cannot change anything at all.
  --------------------------------------------------------------------------- */
  .ed__seg--build { display: none; }

  /* ---------------------------------------------------------------------------
     ...WHICH MEANS THE SUBHEAD NUMBERS HAVE TO RENUMBER THEMSELVES.

     The numbers are authored in the markup (`<span class="mono">03</span>`),
     which is correct — they are content, they are in the no-JS page, and a
     crawler reads them. But hiding one of five leaves the phone reading
     01 · 02 · 04 · 05, and a numbered spine with a hole in it is the single most
     visible kind of wrong: the reader assumes they missed something and scrolls
     back up looking for it.

     A CSS COUNTER FIXES IT WITH NO SECOND SOURCE OF TRUTH, because
     `display: none` DOES NOT INCREMENT A COUNTER. So the phone's numbering is
     derived from what the phone actually shows — hide any subsection and the
     rest renumber, restore it and they renumber back. Nothing has to be kept in
     step by hand, and the desktop keeps the authored numbers untouched.

     `counter-reset` is on `.ed` and not on `.ed__body`: "01 Experience" is a
     direct child of the article and the other four live in the body column, so
     the article is the only box that contains all of them. `.flip` moving the
     skills heading into a card built by JS does not disturb this — counters walk
     the element tree in document order and the card is inserted exactly where
     the heading was.
  --------------------------------------------------------------------------- */
  .ed { counter-reset: edh; }
  .ed__h { counter-increment: edh; }

  /* ...AND THE SUBHEADS GET MORE AIR ABOVE THEM (asked for, of Credentials and
     Skills & tooling specifically).

     `.ed__h`'s `clamp(36px, 6vw, 77px)` floors at 36 on every phone — the same
     "the vw term never reaches its own floor at this width" trap the head plate
     and the footer wordmark have both hit. So a numbered subsection, which is
     the biggest structural break inside a channel, was separated by barely more
     than the 24px between two paragraphs. There is nothing in a phone column but
     vertical space to say "a new part starts here", so it has to be enough to
     read as a break rather than as a gap.

     ONE RULE, NOT TWO NAMED ONES. Credentials and the skills bank are the two
     the owner named because they are the two he was looking at; the fault is in
     the token, and fixing it for one heading and not its siblings is how a
     rhythm stops being a rhythm. `.ed__q + .ed__h`'s own larger value is left
     alone — it still wins where it applies. */
  .ed__body > .ed__h { margin-top: clamp(52px, 13vw, 68px); }
  .ed__body > .ed__h:first-child { margin-top: 0; }
  /* The authored number is replaced, not moved: it stays in the DOM for the
     accessible reading of the heading and simply stops being painted. */
  .ed__h > .mono { display: none; }
  .ed__h::before {
    content: counter(edh, decimal-leading-zero);
    flex: none;
    /* `.ed__h` is a flex row, so this is a flex item and lines up exactly where
       the span it replaces did. The span was `.mono mono--faint`, and a
       pseudo-element inherits the HEADING's font — display face, --t-lg — so
       every one of those properties has to be restated or the number renders as
       a 24px display-type numeral. */
    font-family: var(--f-mono);
    font-size: var(--t-xs);
    font-weight: 500;
    letter-spacing: var(--tr-label);
    line-height: 1;
    color: var(--c-faint);
  }

  /* ---------------------------------------------------------------------------
     THE EXPERIENCE PLATE, TIGHTENED — 733px was 87% of the screen for one fact.

     Nothing is removed: the logo, the title, the three employment facts, the six
     stack tags and the paragraph all stay, because this is the section's
     substance and a recruiter reads all of it. What comes off is the SPACING,
     which was set for a plate ~1.5× this wide.

     THE INSET IS THE PART THAT MATTERS (rule M1). At 24px of padding the plate's
     content started at x=51 while every paragraph above and below it started at
     x=26 — a 25px step in the reading edge, in the middle of the channel, on the
     only full surface on the page. 16px closes it to 17, which is the same
     treatment CH 02's card already takes (14px there, against a list that sets
     its own edge). It cannot go to zero without the plate losing the inset that
     makes it a plate.
  --------------------------------------------------------------------------- */
  /* THE PLATE'S TOP PADDING IS ASYMMETRIC, AND IT HAS TO BE. What sits at the
     top is a centred MASTHEAD, and a masthead needs air above it in a way a
     ranged-left stack does not — centred type with a tight edge above it reads
     as having been pushed up against the frame rather than placed in it. The
     bottom stays tighter because what closes the plate is a paragraph, and a
     paragraph's own descender line is already most of a gap. */
  .role {
    gap: 20px;
    padding: 62px 16px 22px;
  }
  /* The corner arrow is positioned in the padding it no longer has. Both values
     move with it or it floats in from the edge. */
  .role__go { top: 16px; right: 16px; }

  /* ---------------------------------------------------------------------------
     THE IDENTITY IS A MASTHEAD ON A PHONE — centred, and bigger.

     Asked for, and it is the right shape for the width. On a desktop the plate
     is two columns and the logo/title stack is the LEFT one, so it is ranged
     left because it has a neighbour to line up with. Folded there is no
     neighbour: the stack is the full width of the plate with the facts under it,
     and a left-ranged pair of short lines across a 300px card leaves the whole
     right side of the plate's head empty (rule M2 — a block with nothing beside
     it is a sign-off, and a sign-off centres).

     BIGGER FOR THE SAME REASON. At 18px of logo and `--t-bd` of title this was
     the most important fact in the section set at the size of a caption. The
     phone has the width to spend and nothing to spend it against.

     THE FACTS STAY LEFT. They are a label-and-value TABLE — three rows whose
     labels share a column — and centring a table destroys the alignment that
     makes it readable. Centred head over a left-read list is exactly the split
     the site's own M2 rule describes, and it is correct here: the head is the
     masthead, the rows are the data.

     THE LOGO LEADS AND THE ROLE FOLLOWS, WHICH INVERTS THE DESKTOP'S WEIGHTS ON
     PURPOSE. Ranged left in a two-column plate the job title is the larger of
     the two, because the plate is his page and the employer is the answer to
     "where" — but centred, alone, at the head of a card, the mark is doing the
     work a masthead does: it is a shape you recognise before you read anything.
     So the mark is the biggest thing in the plate and the role is set under it
     at a size that reads as its subtitle rather than as its competitor. Two
     centred lines at the same weight would have been the worst of both.
  --------------------------------------------------------------------------- */
  .role__id { text-align: center; }
  .role__at { justify-content: center; }

  /* THE MARK IS SIZED BY ITS WIDTH, AND SETTING ITS HEIGHT STRETCHED IT — that
     is what "the Worldline logo is messed up" was. `.role__logo` is
     `width: clamp(178px, 19vw, 268px)` with `height: auto`, so the viewBox's
     194:20 ratio comes from the width alone. Asking for `height: 34px` on top of
     a fixed width does not scale the mark, it distorts it: 178 wide wants 18.3
     tall, so the whole wordmark was rendered 1.85× too tall. **Change the width
     and let the height follow — never both on a viewBox'd SVG.**

     `min(236px, 78%)` rather than a clamp on vw: the constraint is the PLATE's
     inner width, not the viewport's, and those are two different numbers now
     that the plate has its own padding.

     78 AND NOT 86 BECAUSE OF `.role__go`. The corner arrow is absolutely
     positioned at `right: 16px` and takes ~17px, so the top-right ~33px of the
     plate is spoken for. A mark centred on the FULL width does not know that: at
     86% it ran to x=345 with the arrow starting at 332 and the fan glyph sat
     underneath it. Centred is still centred — the mark is simply narrow enough
     that its end clears the control. Natural height then runs ~19px at 320 to
     ~24px at 390, still well up on the 18.3px it was, and the right shape. */
  .role__logo { width: min(236px, 78%); height: auto; }

  .role__t {
    margin-top: 14px;
    /* THE 18ch CAP HAS TO GO OR CENTRING IS A LIE. `.role__t` carries
       `max-width: 18ch` for the desktop's two-column plate, which made the
       heading a 208px box sitting at the LEFT edge of a 304px container — so
       `text-align: center` centred the words inside that box and the result sat
       visibly left of the logo above it. The measure is not needed here: the
       title is three words in a column that is already narrow. */
    max-width: none;
    /* Deliberately smaller than the mark above it — it is the caption to a
       masthead, not a second headline. */
    font-size: clamp(1.02rem, 4.6vw, 1.24rem);
    line-height: 1.2;
  }
  /* AIR UNDER THE TITLE, asked for. The masthead (mark + role) and the facts
     table are two different KINDS of thing sharing one panel, and at 22px the
     first rule of the table read as an underline belonging to the title rather
     than as the top edge of what follows it. */
  .role__meta { margin-top: 46px; text-align: start; }
  .role__meta > div { padding-block: 11px; }

  /* THE STACK TAGS STAY ABOVE THE DESCRIPTION. Moving them under it was tried at
     the owner's request and put straight back at the owner's read — "it looked
     better before" — which is the right call and worth the note: the chips are
     the scannable answer and the paragraph is the long one, so on a phone, where
     the whole plate cannot be taken in at once, the scannable thing has to come
     first. Underneath, the six chips closed the plate with a block of boxes and
     the prose became the thing between two structures instead of the thing they
     both describe. */
  /* AIR ABOVE THE DESCRIPTION TOO. Its own `clamp(20px, 2.2vw, 28px)` floors at
     20 on a phone, which is the gap between two paragraphs — but what is above
     it here is a row of bordered chips, and a hard edge needs more clearance
     than a baseline does before text set under it stops looking stuck to it.
     (The same reasoning the desktop's `.role__tags` note gives for its own
     bottom spacing; the token just never reaches its top value at this width.) */
  .role__d { margin-top: 26px; line-height: 1.56; }

  /* THE SIX STACK TAGS FIT TWO ROWS INSTEAD OF THREE, AND THE FIX IS THE UNIT
     SIZE, NOT THE LAYOUT (rule M5, the same reasoning as the chip cloud).

     Measured at 390: the tags are 43/82/74/128/59/151px, which wraps 3 / 2 / 1 —
     and that last row, one long tag alone under a half-empty one, is what makes
     a stack strip read as something that has come apart. A grid was not an
     option here for the same reason it was not for the filter bar: "ISO
     8583/20022" is three times the width of "C++" and a shared cell either
     wraps it or strands the short ones.

     SHRINKING THEM WAS TRIED FIRST AND MISSES BY TWO PIXELS, which is why the
     answer is not a size. At 10.4px the six measure 38/72/65/113/52/133 and the
     first four come to 306 in a 304px row — so it still breaks 3/2/1, and any
     value that fits at 390 stops fitting at 360.

     SO THE ROW STRETCHES INSTEAD OF SHRINKING. `flex: 1 1 auto` hands each line's
     leftover width back to the tags on it, so every row ends flush at both edges
     and the strip reads as a block of six rather than as a list that has run
     out. It is the same answer the M5 rule gives (a ragged wrap becomes an
     explicit grid) reached without a grid — a grid would need one cell width for
     labels that run from "C++" to "ISO 8583/20022", and either wraps the long
     one or floats the short ones in the middle of empty cells. Nothing is
     orphaned because there is no ragged edge left for anything to be orphaned
     against, at ANY width. */
  /* Clear of the divider above it and clear of the prose below it. `.role__d`'s
     own top margin (`clamp(20px, 2.2vw, 28px)` = 20 here) is what separates the
     chips from the paragraph, so this only has to set the space above. */
  .role__tags { gap: 6px; margin-top: 4px; }
  .role__tags .tag {
    flex: 1 1 auto;
    justify-content: center;
    padding: 5px 8px;
    font-size: clamp(8.6px, 2.55vw, 10.4px);
  }

  /* ---------------------------------------------------------------------------
     THE CHIP CLOUD, TIGHTENED.

     26 chips over 12 rows with an average of 80px unused at the end of each —
     a quarter of the column — because the labels run from "SQL" to
     "High-availability services" and a wrap fits whole chips or nothing. This
     is one of the few places a ragged edge is correct (a cloud is a cloud, and
     forcing it into a grid would strand the short labels in wide cells), so the
     fix is only to make the units smaller: tighter chips fit more per row and
     the ragged edge gets shorter for free.
  --------------------------------------------------------------------------- */
  /* ---------------------------------------------------------------------------
     "AT A GLANCE": LET THE LABEL COLUMN MEASURE ITSELF.

     The gutter is a hard 76px, chosen because the longest label ("Core stack")
     measured 68 — but `--t-xs` is fluid and that measurement was taken at a
     wider viewport. At 390 the label renders 79px, so the one row the number
     was sized for is the one row that wraps, and it wraps into two lines beside
     a one-line value.

     `max-content` WAS TRIED AND IS WRONG, which is worth writing down because
     it is the obvious answer: every `.ed__fact` is its OWN grid container, so
     the track is sized per row rather than across the card. The labels came out
     43 / 85 / 77 / 51 / 60px and the VALUES no longer shared a left edge — the
     alignment that made it a spec strip was the only thing the hard number was
     really buying. So it stays a hard number and is simply corrected to clear
     the real 85px measurement.
  --------------------------------------------------------------------------- */
  .ed__fact { grid-template-columns: 88px minmax(0, 1fr); }
  .ed__fact dt { white-space: nowrap; }

  /* ---------------------------------------------------------------------------
     THE FLIP CARD. `initFlip()` in app.js builds the wrapper; this is the whole
     of its appearance.

     A REAL FLIP, NOT A CROSS-FADE, because the two faces are the same KIND of
     thing — reference you consult, not content you read — and turning something
     over is how you say "the other side of this object" rather than "a different
     object". A fade would read as a swap between two panels; the rotation says
     they are one.

     THE CARD IS AS TALL AS ITS TALLER FACE and that is deliberate rather than
     conceded. Animating the height as well as the rotation is two things moving
     on one object, and it makes the page under the card jump while it turns.
     The two faces measure within ~40px of each other here, so the slack is
     invisible; if a face is ever added that is much taller, THAT is the thing to
     fix, not this rule.
  --------------------------------------------------------------------------- */
  .flip {
    perspective: 1600px;
    /* THE CARD IS A SUBSECTION, SO IT TAKES A SUBSECTION'S AIR ABOVE IT.

       It inherited `.kit`'s own 18px top margin, which was correct when the
       heading sat above the bank in the prose column and the two were one unit.
       Inside the card the heading is the card's first line, so 18px was the gap
       between the END of Credentials and the START of a new numbered subsection
       — every other one of those on this page gets `clamp(36px, 6vw, 77px)`.
       The owner read it as the padding above "Skills & tooling" being too low,
       which is exactly what it was. Matched to what `.ed__h` takes on a phone
       (above), so the card starts on the same beat every other numbered
       subsection in the column starts on. */
    margin-top: clamp(52px, 13vw, 68px);
  }
  .flip__in {
    display: grid;
    /* THE SAME `min-width` TRAP AS THE FILTER STRIP, ONE LEVEL DEEPER — and it
       bit again here for exactly the reason it bit the first time. `.solo__set`
       is a horizontal scroller whose min-content width is still the sum of its
       six buttons, and this card puts TWO fresh grid boxes between it and the
       nearest ancestor that was allowed to shrink. The faces measured 556px
       inside a 338px card until both tracks were told they may be narrower than
       their content wants.

       An `auto` column and an `auto` grid item are the two places this can go
       wrong; both are pinned. */
    grid-template-columns: minmax(0, 1fr);
    transform-style: preserve-3d;
    transition: transform 620ms var(--e-inout);
  }
  .flip[data-face='b'] .flip__in { transform: rotateY(180deg); }

  /* Both faces share ONE grid cell, which is what makes the card self-size to
     the taller of the two without a measured height anywhere. */
  .flip__face {
    grid-area: 1 / 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    -webkit-backface-visibility: hidden;
            backface-visibility: hidden;
  }
  .flip__face--b { transform: rotateY(180deg); }

  /* ---------------------------------------------------------------------------
     THE TURN HAS TO BE FREE, AND A 3D ROTATION IS ONLY FREE IF THE BROWSER CAN
     KEEP BOTH FACES AS FINISHED TEXTURES AND SPIN THEM.

     It could not. Three things were making the compositor re-rasterise the card
     on almost every frame of the 620ms:

       1. The card CHANGED HEIGHT mid-turn, because the two faces were different
          heights and the grid cell tracked whichever one was in front. A size
          change is layout, and layout during a transform animation is the one
          thing that guarantees a stutter. The skills bank is now a scroller
          (below), so the two faces are the same box and the card never resizes.
       2. Nothing was promoted, so the first frame of every turn was spent
             creating layers for two large subtrees.
       3. `.ed__aside::before` — the faceplate grid — was having its
          `background-position` rewritten by the scroll handler on every frame,
          inside the card. That is fixed at source in app.js: the drift exists to
          give a PINNED card a moving floor, and below 1000px the card is not
          pinned, so the write is now skipped entirely.

     `will-change` IS SET FOR THE DURATION AND THEN TAKEN AWAY. `.turning` is
     added by app.js on press and removed when the transition ends. A permanent
     `will-change: transform` on a card holding 26 chips and a six-row table
     means the browser holds those layers in memory for the whole visit, on the
     device least able to afford it — and a hint that is always on is a hint that
     is never information.
  --------------------------------------------------------------------------- */
  .flip.turning .flip__in { will-change: transform; }
  .flip.turning .flip__face { backface-visibility: hidden; }

  /* ---------------------------------------------------------------------------
     THE SKILLS BANK IS A FIXED WINDOW THAT SCROLLS INSIDE ITSELF.

     Asked for directly: "can we have fixed height... and maybe scrollable inside
     element, this way I can have even more skills added without the element
     having to be bigger." That is the right instinct and it is worth saying why
     it is not the usual mistake. THE SITE'S OWN RULE IS THAT A NESTED SCROLLER
     IS THE WORST CONTROL ON A PHONE — it swallows the flick you meant for the
     page — and that rule is why the work index was uncapped two channels down.
     The difference here is that this pane is not in the page's scroll path: it
     is one face of a card the reader has deliberately turned to, it is a
     REFERENCE table rather than a step in the argument, and every one of the six
     categories can be brought into the window by the filter bar directly above
     it without scrolling at all.

     THE HEIGHT IS NOT A NUMBER. `flex: 1` inside the face plus a cap: the bank
     takes whatever height the other face needs, so the two faces are identical
     boxes (which is what makes the turn free) and the cap only bites if the
     other face is ever taller than the cap. Adding a 27th chip changes nothing
     about the card — which is the whole request.

     THE READER CANNOT SCROLL IT; THE FILTER BAR CAN. `overflow-y: hidden`, at
     the owner's instruction — "no need to allow user to scroll in, as it blocks
     website scroll, so let the carousel or skill button be it". That is the
     site's own rule about nested scrollers, correctly applied: a pane inside the
     page's scroll path swallows the flick you meant for the page, and this one
     sits in the middle of a fifteen-screen document.

     `hidden` DOES NOT MEAN UNSCROLLABLE — IT MEANS UNSCROLLABLE *BY THE USER*.
     `scrollTo()` still moves it, so `followY()` in initKit goes on bringing the
     armed category to the top of the window exactly as before, and `scroll`
     events still fire so the edge fades still know where they are. The pane
     becomes a display the buttons drive rather than a control competing with the
     page — which is what the bank always was.
  --------------------------------------------------------------------------- */
  .flip__face .kit {
    display: flex;
    flex-direction: column;
    /* A flex item's automatic minimum size is its content, so without this the
       bank refuses to be shorter than 26 chips and the cap does nothing. */
    min-height: 0;
    overflow: hidden;       /* the radius has to clip the scroller's edges */
  }
  .flip__face .kit__bar { flex: none; }
  .flip__face .kit__cloud {
    flex: 1;
    min-height: 0;
    max-height: min(46vh, 320px);
    overflow-y: hidden;
    scrollbar-width: none;
    /* Chips wrap from the top of the pane rather than being centred in it — a
       centred cloud inside a fixed window leaves uneven air at both ends and
       reads as content that has slipped. */
    align-content: flex-start;
    /* THE FADES. Two edge ramps driven by two custom properties, so ONE mask
       declaration covers all four states instead of four mask declarations
       fighting over the cascade. At 0px the ramp has no length, which is a hard
       edge — so "no fade" needs no rule of its own.

       `initScroller` in app.js writes `.more-a` (there is content above) and
       `.more-b` (there is content below); this is the same pair the record crate
       and the work index already use, so the phone learns one signal once. They
       still fire with `overflow: hidden` — a programmatic scroll dispatches a
       `scroll` event like any other.

       DELIBERATELY SHALLOW, as asked: 14px at the bottom and 10 at the top out
       of a ~320px pane. A fade deep enough to be decorative starts dimming the
       row you are reading; this one only has to say the pane has not ended.

       NO TRANSITION ON THE RAMPS. A custom property is not animatable unless it
       has been registered with `@property`, which is a top-level at-rule and
       cannot live inside a media query — so registering one here would put an
       unscoped rule in the one file whose guarantee is that it has none. The
       fades appear on the frame the pane is scrolled, which is when they are
       true. */
    --fd-t: 0px;
    --fd-b: 0px;
    -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 var(--fd-t),
                        #000 calc(100% - var(--fd-b)), transparent 100%);
            mask-image: linear-gradient(180deg, transparent 0, #000 var(--fd-t),
                        #000 calc(100% - var(--fd-b)), transparent 100%);
  }
  .flip__face .kit__cloud::-webkit-scrollbar { display: none; }
  .flip__face .kit__cloud.more-a { --fd-t: 10px; }
  .flip__face .kit__cloud.more-b { --fd-b: 14px; }

  /* ---------------------------------------------------------------------------
     THE CONTROL ROW — ONE SHAPE, TWO JOBS, SPLIT 70/30.

     Asked for as: split the flip button in two so the At-a-glance face can carry
     the résumé button as well, and on the skills face let the whole row turn.
     That is a better row than the one it replaces for a reason worth writing
     down — the At-a-glance face used to end with TWO stacked full-width
     controls, the résumé button that belongs to the card and the flip button
     that belongs to the card's frame, ~110px of chrome under six facts. One row
     with the action at 70% and the turn at 30% says the same two things in half
     the height, and it gives both faces the same footer, which is the other
     reason the turn is smooth: the rows are the same box.

     THE TWO FACES ARE STRUCTURED DIFFERENTLY AND THAT IS DELIBERATE. On the
     At-a-glance face the two halves do different things, so they are two real
     controls (a link and a button). On the skills face they do the SAME thing,
     so it is ONE button that happens to be drawn in two parts — the alternative,
     two buttons with one action, is either two tab stops for one outcome or an
     `aria-hidden` element that is still focusable. A divider is cheaper than
     both and the row looks identical either way.
  --------------------------------------------------------------------------- */
  .flip__ctl {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 30%;
    align-items: stretch;
    gap: 8px;
    margin-top: 12px;
  }
  /* The résumé button spans its own cell rather than sitting `inline-flex` in
     the middle of it. `.btn` is already the right object; it only has to stop
     being an inline one. */
  .flip__ctl .btn { display: flex; width: 100%; margin-top: 0; min-height: 46px; }

  /* The turn control, in both its forms. */
  .flip__turn,
  .flip__ctl--a {
    display: flex; align-items: center; justify-content: center;
    min-height: 46px;
    border: 1px solid var(--c-line-2);
    border-radius: var(--r-ctl);
    background: var(--c-glass);
    color: var(--c-fg);
    font-family: var(--f-mono);
    font-size: var(--t-xs);
    font-weight: 500;
    letter-spacing: var(--tr-label);
    text-transform: uppercase;
    transition: border-color var(--d-2), background var(--d-2);
  }
  .flip__turn:active,
  .flip__ctl--a:active { background: var(--c-glass-2); }

  /* THE SKILLS FACE: one button, two halves, a hairline between them. The 70/30
     split is the grid; the divider is a border on the icon half, so the two
     faces' rows line up to the pixel even though one has a real 8px gap in it
     and the other does not. */
  .flip__ctl--a {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 30%;
    /* STATED, NOT INHERITED. A `<button>` centres its content box by UA rule, so
       the two halves came out as tall as their own line boxes and the divider
       between them rendered as a 13px tick floating in a 46px control. */
    align-items: stretch;
    gap: 0;
    padding: 0;
    margin-top: 12px;
    text-align: center;
  }
  .flip__ctl--a > span {
    display: flex; align-items: center; justify-content: center; gap: 10px;
    align-self: stretch;
    padding-inline: 10px;
  }
  .flip__ctl--a > .flip__ico { border-left: 1px solid var(--c-line); }

  /* The icon is the site's rotate mark, not `#i-arr` — an arrow means "leaves
     the page" everywhere else here and this control never does. */
  .flip__ico .ico { color: var(--c-faint); transition: color var(--d-2), transform 520ms var(--e-inout); }
  .flip__ctl--a:active .flip__ico .ico,
  .flip__turn:active .ico { color: var(--c-accent-txt); transform: rotate(-180deg); }
  .flip__turn .ico { color: var(--c-faint); transition: color var(--d-2), transform 520ms var(--e-inout); }
  .flip__turn { padding: 0; }
  /* `backface-visibility` hides the back face but does NOT stop it catching a
     tap — Safari in particular will happily deliver a click to a surface facing
     away from you. `inert` (set in JS) covers keyboard and assistive tech;
     this covers the pointer. */
  .flip[data-face='a'] .flip__face--b,
  .flip[data-face='b'] .flip__face--a { pointer-events: none; }

  /* ...AND THE FACTS SPREAD BACK INTO WHATEVER SLACK THE OTHER FACE LEAVES.

     `align-content: start` is right for a panel sized by its contents, which is
     what the aside is everywhere else below 1000px. Inside the card it is not
     sized by its contents — it is stretched to the taller face — so `start`
     stacked six rows at the top and left ~90px of empty plate under them, on the
     face that is supposed to be the tidier of the two.

     `space-between` is the rule the DESKTOP card uses, and for exactly this
     reason: it exists to share out leftover height. The card has leftover height
     again, so the rule comes back with it. Scoped to the face so the plain
     stacked panel at 900–999px is untouched. */
  .flip__face--b .ed__aside,
  .flip__face--b .ed__facts { align-content: space-between; }

  /* The two panels were laid out as separate blocks with their own top margins.
     Inside a face they are the face. */
  .flip__face .ed__h { margin-top: 0; }
  .flip__face .kit,
  .flip__face .ed__aside { margin-top: 12px; }
  /* `1fr` so whichever panel is shorter grows to the card's height instead of
     leaving its footer floating half way up the taller face's outline. */
  .flip__face .kit,
  .flip__face .ed__aside { flex: 1; }

  /* THE ROTATION IS A LITERAL DURATION, SO `--motion` CANNOT REACH IT. The
     site's reduced-motion handling works by collapsing that token, which only
     touches transitions written in terms of it — this one is `620ms` because a
     card turning over has a physical speed that should not scale with an
     unrelated global. That means it has to be killed by name, exactly as the
     forever-looping keyframes are. The card still flips; it just arrives
     already turned, which is what someone who asked for less motion wants. */
  @media (prefers-reduced-motion: reduce) {
    .flip__in { transition: none; }
    .flip__ico .ico,
    .flip__turn .ico { transition: none; }
  }

  .kit__cloud { padding: 14px; gap: 7px; }
  .kit__cloud .chip {
    padding: 6px 10px;
    gap: 6px;
    font-size: clamp(0.78rem, 3.2vw, 0.88rem);
  }
}

/* =============================================================================
   CH 02 — SELECTED WORK
============================================================================= */
@media (max-width: 899px) {
  /* ---------------------------------------------------------------------------
     ONE PLATE, THREE LEFT EDGES (rule M1).

     Folded for a phone, the index and the project card are stacked inside a
     single bordered plate — correctly, they are one object. But the index rows
     are padded 14px and the card 24px, so reading down the plate the text
     stepped 41 (row number) → 70 (row title) → 51 (everything in the card).
     Two of those are a deliberate internal grid; the third is just a different
     component's padding showing through, and it is the one the eye catches,
     because it makes the card look slightly indented from the list that
     introduces it.

     Matching the card's inset to the rows' puts the card's content on the same
     line as the row numbers — the plate's true left edge — and the two halves
     finally read as one rack. The block padding stays generous: it is the
     horizontal edge that has to agree, not the vertical rhythm.
  --------------------------------------------------------------------------- */
  .wkx .reel__act { padding-inline: 14px; }

  /* ---------------------------------------------------------------------------
     THE ACCORDION. `initWkx()` moves each act inside the row it describes and
     puts `.wkx__index--fold` on the list; this is the whole of its appearance.

     COLLAPSED WITH `grid-template-rows: 0fr → 1fr`, which is the only way to
     animate to a height nobody has measured. `max-height` needs a guessed cap:
     too small and the tallest project is clipped, too large and the transition
     spends most of its duration animating empty space, so the row appears to
     stall before it opens. The grid technique animates to the content's real
     height, whatever that turns out to be. Where it is unsupported the row
     simply snaps open, which is a fine floor.

     The act needs `min-height: 0` AND `overflow: hidden` for the collapse to
     actually reach zero — a grid item's automatic minimum size is its content,
     so without them a `0fr` track still renders full height and nothing appears
     to close at all.
  --------------------------------------------------------------------------- */
  /* THE LIST STOPS SHARING ITS HEIGHT OUT EQUALLY. Its base rule is
     `grid-auto-rows: minmax(52px, 1fr)`, which exists so six short strips fill
     the plate's bay beside the card on a desktop. With a panel now living inside
     each row, `1fr` split the whole opened height six ways and every row came
     out 414px tall — five of them holding a 52px strip and 362px of nothing.
     Rows size to their own content here.

     `.js .wkx__index--fold`, not `.wkx__index--fold`: the rule being overridden
     is `.js .wkx__index`, two classes, and a media query adds no specificity —
     being in a later sheet only breaks a TIE. One class loses outright, which is
     exactly what happened on the first attempt (every row stayed 406px). */
  .js .wkx__index--fold { grid-auto-rows: auto; }
  /* …AND THE 52px THAT `minmax()` WAS ALSO PROVIDING HAS TO COME BACK. That
     track had two jobs — share the bay AND floor the row — and dropping to
     `auto` silently gave up both: closed rows measured 41px, under every
     platform's 44px minimum, on a control you are meant to press. */
  .js .wkx__index--fold .wkx__b { min-height: 52px; }

  .wkx__index--fold li {
    display: grid;
    grid-template-rows: auto 0fr;
    transition: grid-template-rows 460ms var(--e-inout);
  }
  .wkx__index--fold li:has(.reel__act.on) { grid-template-rows: auto 1fr; }
  .wkx__index--fold .reel__act {
    min-height: 0;
    overflow: hidden;
    /* Out of the stage's one-cell stack and back into normal flow — inside a row
       there is nothing to stack against. */
    grid-area: auto;
    opacity: 1;
    /* PADDING SURVIVES A COLLAPSED TRACK. `0fr` plus `overflow: hidden` takes the
       CONTENT box to nothing, but the box's own padding is not content — a closed
       row still measured 48px, which is this element's 24px top and bottom. The
       padding has to travel with the height, so it is animated on the same curve
       and the row closes to an actual hairline. */
    padding-block: 0;
    transition: padding-block 460ms var(--e-inout);
  }
  .wkx__index--fold li:has(.reel__act.on) .reel__act { padding-block: 20px; }
  /* The panel is the row opened, not a card dropped into it, so it takes the
     channel's own tone and a hairline rather than a second border. */
  .wkx__index--fold .reel__act > * { padding-block: 2px; }
  .wkx__index--fold li:has(.reel__act.on) .wkx__b { background: var(--c-glass-2); }

  /* The stage is empty once the acts have moved out of it. */
  .wkx__index--fold ~ .reel__stage,
  .wkx .reel__stage:empty { display: none; }

  /* ---------------------------------------------------------------------------
     THE CUT BAND COMES OFF THE PHONE, AND IT IS THE ONE THING IN HERE THAT WAS
     ACTIVELY FIGHTING THE SCROLL.

     `.reel__sweep` is an amber gradient the width of the whole plate, translated
     across it over 720ms on every cut. On a desktop it is exactly right: the
     card swaps its contents in place, and the band is the edit — the frame you
     cut on. Folded, THERE IS NO CUT LEFT TO MARK. The row opening IS the
     transition, and the band is a large translucent layer being composited over
     a plate that is simultaneously running a layout animation, 5.6 seconds
     apart, forever, while the reader is scrolling through it.

     Two costs, and the second is the one that was felt: it repaints ~650×340 of
     phone screen for most of a second, and it is a second, unrelated motion
     crossing a row that is already moving. The owner's read was "some scroll
     animation is interfering" — this is it.
  --------------------------------------------------------------------------- */
  .wkx .reel__sweep { display: none; }

  /* THE OPEN ROW IS ITS OWN LAYOUT AND PAINT BOX. Both are true — the act is
     `overflow: hidden` with a known containing block — and stating them lets the
     browser keep the 460ms height animation's invalidation inside the row
     instead of walking the rest of the channel every frame. `style` is left OFF
     deliberately: the reel writes `--dwell` on the plate and the counters live
     inside these rows. */
  .wkx__index--fold .reel__act { contain: layout paint; }

  /* THE PER-ACT STAGGER IS COMPRESSED. Each act's four blocks arrive at 120 /
     190 / 260 / 330ms and then take their own ~400ms, so the last line of a card
     was still settling ~730ms after a row that opens in 460 had finished
     opening. On a desktop that reads as the card assembling; inside a row that
     has already stopped moving it reads as the content lagging the box. */
  .js .wkx__index--fold .reel__act > .reel__stamp { --rd: 60ms; }
  .js .wkx__index--fold .reel__act > .reel__t     { --rd: 105ms; }
  .js .wkx__index--fold .reel__act > .reel__d     { --rd: 150ms; }
  .js .wkx__index--fold .reel__act > .reel__foot  { --rd: 195ms; }

  /* THE DWELL BAR IS THE ONE THING THAT STILL HAS TO READ AS A CLOCK. It is
     `.wkx__fill` on the armed row, and with the row now also carrying an open
     panel underneath it, the bar has to stay pinned to the row's own strip
     rather than stretching down the whole opened cell. */
  .wkx__index--fold .wkx__b { position: relative; }

  @media (prefers-reduced-motion: reduce) {
    .wkx__index--fold li { transition: none; }
  }
}

/* =============================================================================
   CH 03 — FREELANCE
============================================================================= */
@media (max-width: 899px) {
  /* ---------------------------------------------------------------------------
     THE TERMS STRIP IS A SPEC TABLE ON A PHONE, NOT A WRAPPED ROW.

     On a wide screen the three terms sit side by side and each is read as one
     unit — "Scope: fixed price" — so label and value riding together inline is
     right, and the existing rule keeps them from ever being split by a wrap.

     Stacked, that same construction turns into three rows whose values start at
     78, 95 and 112px, because each one begins wherever its own label happened
     to end. Three lines of prose down a staircase is the most visible kind of
     misalignment there is, and the block is supposed to read as the terms
     TABLE that closes the section.

     A grid gives the labels a shared column and the values a shared left edge,
     and it keeps the guarantee the inline version was built for: label and
     value are still one row, so a wrap can still never strand "Ownership" on a
     line of its own. 84px clears the longest label (77px).
  --------------------------------------------------------------------------- */
  .terms {
    display: grid;
    gap: 0;
    padding-top: 0;
  }
  .terms__i {
    display: grid;
    grid-template-columns: 84px minmax(0, 1fr);
    align-items: baseline;
    gap: var(--s-3);
    padding-block: 12px;
    border-top: 1px solid var(--c-line);
  }
}

/* Same reasoning as the "At a glance" rows one channel up: below 360 the label
   column costs the value more than the alignment is worth. */
@media (max-width: 360px) {
  .terms__i {
    grid-template-columns: minmax(0, 1fr);
    gap: 3px;
  }
}

/* =============================================================================
   CH 04 — STUDIO
============================================================================= */
@media (max-width: 899px) {
  /* ---------------------------------------------------------------------------
     THE CONTACT SHEET GOES BACK TO BEING A CONTACT SHEET.

     The sheet is four cells on a 2×2 grid — three frames and a legend in the
     fourth — and on a wide screen that is exactly right: it completes the
     rectangle, which is the whole reason the legend exists rather than the
     corner being left empty.

     Folded onto a phone the same grid produces the worst block on the site.
     Frames 01 and 02 take row one; frame 03 sits alone in the bottom left; and
     the legend, which has no frame and is bottom-aligned in its cell, floats in
     the bottom right at a height that matches nothing beside it. So the section
     about a room reads as a layout with a hole in it, and the three frames —
     which are placeholders until real photographs arrive — spend ~600px of a
     844px screen saying nothing.

     THE FIX IS TO STOP FIGHTING THE FORMAT. A contact sheet is a strip of small
     proofs; it does not want to be two big squares per row on a phone, it wants
     to be all three in one row at proof size, which is 104px at 390 and 82 at
     320. That is one tidy row instead of two ragged ones, no orphan, no empty
     cell, and it costs ~140px where the 2×2 cost ~600.

     The legend then becomes what a caption bar under a sheet always is: full
     width, one rule, the range at one end and the place at the other.
  --------------------------------------------------------------------------- */
  .art__sheet {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 10px;
  }
  .art__fig { gap: 7px; }
  .art__legend {
    grid-column: 1 / -1;
    flex-direction: row;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--s-4);
    margin-top: 4px;
    padding-top: 10px;
    padding-bottom: 0;
    border-top: 1px solid var(--c-line);
  }
  /* The proof number is fixed at 10px from a corner it now shares with a 104px
     cell instead of a 155px one, so it pulls in with the frame. */
  .art__no { top: 6px; left: 6px; padding: 3px 5px; }

  /* Three captions across a 338px row: "The chain" is 62px inside a 104px cell
     at 390 and still fits at 320, but a longer one added later should shrink
     rather than wrap under a square it no longer matches. */
  .art__fig figcaption { font-size: clamp(9px, 2.6vw, 11.36px); }
}

/* =============================================================================
   CH 05 — MUSIC
============================================================================= */
@media (max-width: 899px) {
  /* ---------------------------------------------------------------------------
     THE TRANSPORT WAS THREE FRAGMENTS. IT IS ONE STRIP AGAIN.

     The strip is a flex row that wraps on a phone, and the rule that makes the
     showing readout take the first row gives it `flex: 1 1 100%`. A 100% basis
     is a whole row BY ITSELF — so the lamp, which comes before it, was pushed
     onto a row of its own. What the reader got was a lone dot, then the cue
     text under it, then the time and the mute button pushed to the far right of
     a third band: three pieces of one control, none of them next to the thing
     they describe. The lamp says whether anything is playing and it was nowhere
     near the title of what is playing.

     Wrapping cannot express "these two go together and those two go together" —
     it only knows what fits. So the strip becomes an explicit two-row grid,
     which is what it was always trying to be:

         ●  Giga Chad (Vocal Edit)
         0:00 / 0:00              [mute]

     state and subject on one line, position and control on the next.

     THE CUE AND THE TITLE STILL SHARE ONE SLOT — they are placed in the same
     grid area, which is the same arrangement the flex version had and for the
     same reason: the cue is interface and the title is content, only one is
     ever true, and the hidden one keeps its zero max-width so it cannot size
     the row. Being in one cell also means the strip does not change shape when
     a preview starts.

     The waveform stays hidden here (it already was, below 860px). 84 bars in
     the ~160px this layout could spare is the "row of 5px dashes" the flat-line
     rest state was written to avoid — at this width the time readout IS the
     position display.
  --------------------------------------------------------------------------- */
  /* ...AND THEN IT WENT TO ONE ROW, because two was still one too many.

     The owner: "the waveform container in tracks is not adapted for mobile, it
     takes 2 lines and does not look good — maybe remove the element or hide it."
     The two-row grid above was already an improvement on the three ragged
     fragments the flex wrap produced, but it is still a transport occupying two
     bands under the shelf for a component whose entire job is a 30-second
     preview, and at rest both bands read as placeholders: "TAP A SLEEVE TO PLAY"
     over "0:00 / 0:00".

     HIDING THE WHOLE STRIP WOULD TAKE THE STOP BUTTON WITH IT, and that is the
     one control the shelf has — a preview you cannot stop is worse than a
     transport you find slightly tall. So the ROW that carries no information at
     rest goes instead: the time readout.

     NOTHING IS LOST, BECAUSE POSITION IS ALREADY DRAWN WHERE YOU ARE LOOKING.
     `.cut__bar` is a 4px fill across the bottom of the PLAYING SLEEVE's artwork,
     which is both a better place for it (on the thing it describes) and the only
     one of the two a thumb is anywhere near. The clock was the desktop's answer
     to the same question, sitting next to a waveform that is itself hidden below
     860px — so on a phone it was a readout beside a readout that is not there.

     What is left is one line: state, subject, control. */
  .rec__np {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas: "lamp txt stop";
    align-items: center;
    column-gap: var(--s-3);
    row-gap: 0;
  }
  .rec__lamp { grid-area: lamp; }
  .rec__cue,
  .rec__now {
    grid-area: txt;
    max-width: 100%;
    /* One line, and a long title gives way at its end rather than pushing the
       stop button off the strip. */
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .rec__time { display: none; }
  .rec__stop { grid-area: stop; justify-self: end; margin-left: 0; }

  /* ---------------------------------------------------------------------------
     THE SUBSCRIBE BLOCK NEEDS TO STOP BEING PART OF THE VIDEO ABOVE IT.

     On a desktop it is a THIRD CELL of the bench grid — column one, row two, in
     the space the feature frame leaves under itself beside the two-frame stack.
     It has no padding of its own on purpose (`.subx`'s own comment: the row
     supplies the height and `place-content: center` puts the type in the middle
     of it), and that is right when the block is sitting in a hole that already
     has air on every side.

     Stacked, the grid collapses to one column and the hole disappears. What is
     left is a 14px grid gap between a video's date stamp and a 40px SUBSCRIBE —
     so the word reads as a caption belonging to the clip above it rather than as
     the channel's own call to action, which is exactly what the owner was
     seeing. Measured: 14px above, and the block then closes the section with the
     channel's full 46px of bottom padding under it — lopsided both ways.

     A RULE, NOT JUST A MARGIN. The gap alone would separate it; the hairline
     says what the separation MEANS — everything above is the catalogue, this is
     the one thing being asked for. The space above is deliberately larger than
     the space below, because the block belongs to the section's foot: a
     sign-off sits nearer what closes than what precedes it.
  --------------------------------------------------------------------------- */
  /* =========================================================================
     THE VIDEO CAROUSEL. `initVidx()` moves the three frames into `.vidx` and
     builds the transport under it; this is the whole of its appearance.

     THE TRACK IS A NATIVE SCROLLER WITH SNAP POINTS. Everything that makes a
     carousel feel good on a phone — the momentum, the rubber band at the ends,
     the way a hard flick crosses two slides and a soft one falls back — is the
     platform's, and it is better than anything a transform-driven track can
     imitate. All this stylesheet does is set the slide width and where the snap
     lands; the JS never touches a transform.
  ========================================================================= */
  .vidx {
    display: flex;
    gap: var(--s-4);
    /* FULL-BLEED, AND THIS IS THE ONE PLACE M1's "ONE LEFT EDGE" IS SERVED BY
       BREAKING OUT RATHER THAN LINING UP. A carousel has to say there is
       something beside the current slide, and the only honest way to say it is
       to let the next frame's edge appear at the screen boundary. The scroll
       padding puts the SLIDE back on the content column, so the picture still
       starts on the same line as every paragraph in the channel — it is the
       TRACK that runs wide, not the content in it. */
    margin-inline: calc(var(--gut) * -1);
    padding-inline: var(--gut);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--gut);
    /* A sideways flick must not be taken as a browser back-swipe. */
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    /* The scrollbar is suppressed because the transport underneath already says
       where you are, twice — a readout and three dots. */
  }
  .vidx::-webkit-scrollbar { display: none; }
  .vidx > .vid {
    /* NARROWER THAN THE COLUMN BY EXACTLY THE PEEK. A carousel that fills the
       screen edge to edge is indistinguishable from a single image until it
       moves — the affordance IS the sliver of the next frame at the right edge.
       26px is the gutter, so the peek is the same width as the margin the eye
       is already using as a unit. The slide still starts on the content
       column's left edge, which is the line every paragraph in the channel
       starts on. */
    flex: 0 0 calc(100% - 26px);
    scroll-snap-align: start;
    scroll-snap-stop: always;   /* one slide per flick, never a blur of three */
  }

  /* THE SLIDE OFF-CENTRE IS DIMMED AND SLIGHTLY BACK, WHICH IS WHAT MAKES THE
     SCROLL READ AS DEPTH RATHER THAN AS A ROW SLIDING PAST.

     Scale, not translate, and only 2% of it: enough that the incoming frame
     resolves toward you as it lands, small enough that it never reads as a
     zoom. The transition is on the SLIDE and driven by a class the JS writes at
     the snap point, so the two frames either side of the boundary cross over
     once, at rest — not on every frame of the drag, which is what makes
     scroll-linked scaling stutter. */
  .vidx > .vid {
    transition: opacity 420ms var(--e-out), transform 520ms var(--e-out);
    transform: scale(.975);
    opacity: .42;
  }
  .vidx > .vid.on { transform: none; opacity: 1; }

  /* ---- the transport ------------------------------------------------------
     Index readout, dwell dots, two channel buttons — CH 02's rack in a
     different axis. It is deliberately NOT a bar under a picture: it is the
     same instrument the rest of the page is drawn as.
  ------------------------------------------------------------------------- */
  /* THE TRANSPORT BELONGS TO THE CAROUSEL, SO IT HAS TO SIT CLOSER TO IT THAN
     THE CAROUSEL SITS TO WHAT FOLLOWS.

     It is a sibling of the track inside `.bench__grid`, so it was inheriting the
     grid's own block gap (28px) on top of its own 16px margin — 44px between a
     clip's date stamp and a strip of controls that describe that clip, against
     28px between the whole carousel and the Subscribe sign under it. The
     controls were further from their subject than the subject was from the next
     thing entirely.

     AND THE MEASURED GAP UNDERSTATES IT, which is why 16px looked like more than
     16px: the dots and the channel buttons are 44px tall for the finger, but the
     INK in them is a 2px bar centred in that box, so there is another 21px of
     invisible target above the first thing you can see. What the eye reported as
     65px of hole was 28 + 16 + 21.

     So the grid's gap is cancelled and a deliberate 14 is put back — measured to
     the ink, that lands the transport ~35px under the date, which is close
     enough to read as attached and far enough not to crowd it. `--bench-gap` is
     the same custom property the gap itself is set from, so the two cannot drift
     apart. */
  .vidx__bar {
    display: flex; align-items: center; gap: var(--s-4);
    margin-top: calc(14px - var(--bench-gap));
  }
  .vidx__idx { color: var(--c-faint); font-variant-numeric: tabular-nums; }
  .vidx__idx b { color: var(--c-fg); font-weight: 500; }
  .vidx__idx i { font-style: normal; padding-inline: 4px; opacity: .6; }

  .vidx__dots { display: flex; align-items: center; gap: 6px; flex: 1; }
  /* A DOT IS A TRACK, NOT A CIRCLE, because the armed one has a clock running
     through it. Round dots cannot show a duration; a 2px bar can, and it is the
     same `.wkx__fill` idea the work rack already uses — one language. */
  /* THE BUTTON IS 44px OF FINGER; THE MARK INSIDE IT IS 2px OF INK. Both layers
     are drawn by pseudo/child elements centred in that box rather than by
     sizing the button down to the bar — the tap-target audit measures the
     ELEMENT's rect, and a 2px-tall control with a generous `::after` passes by
     eye and fails on the numbers, which is the same thing as failing. */
  .vidx__dot {
    flex: 1 1 auto;
    min-width: 38px;
    max-width: 46px;
    height: 44px;
    position: relative;
    padding: 0; border: 0; background: none;
  }

  /* THE READOUT GIVES WAY ON THE NARROWEST PHONES, NOT THE TARGETS.

     At 320 the bar's fixed content — the "01 / 03" readout at ~56px, two 44px
     channel buttons and three gaps — leaves 84px for three dots, which is 26px
     each: under the 38px floor, on a control whose whole job is being pressed.

     The readout is the half that can go. It says the same thing the dots say,
     in the same strip, at the same moment — three marks with one lit IS "01 of
     03", and it is the version you can read without focusing. Dropping it hands
     the dots 45px each. 380 rather than 360, because at 360 they measure 32 and
     that is still short. */
  @media (max-width: 379px) {
    .vidx__idx { display: none; }
  }
  .vidx__dot::before,
  .vidx__fill {
    content: '';
    position: absolute; left: 0; right: 0; top: 50%;
    margin-top: -1px; height: 2px; border-radius: 2px;
  }
  .vidx__dot::before { background: var(--c-line-2); transition: background var(--d-2); }
  .vidx__dot.on::before { background: color-mix(in srgb, var(--c-fg) 26%, transparent); }
  .vidx__fill {
    background: var(--c-accent);
    transform: scaleX(0); transform-origin: left;
  }
  /* Only the armed dot runs a clock, and only while the reel is actually
     playing — `.taken` is set the moment the reader drives it themselves, and
     from then on the dots are a position readout with no timer behind them. */
  .vidx__dot.on .vidx__fill { animation: vidxFill var(--vidx-dwell, 5200ms) linear forwards; }
  .vidx__bar.taken .vidx__fill { animation: none; opacity: 0; }
  @keyframes vidxFill { from { transform: scaleX(0); } to { transform: scaleX(1); } }

  .vidx__ctl { display: flex; gap: 8px; }
  .vidx__nav {
    display: flex; align-items: center; justify-content: center;
    width: 44px; height: 44px;
    border: 1px solid var(--c-line-2);
    border-radius: var(--r-ctl);
    background: var(--c-glass);
    color: var(--c-muted);
    transition: color var(--d-2), border-color var(--d-2), background var(--d-2);
  }
  .vidx__nav:active { background: var(--c-glass-2); color: var(--c-fg); }
  /* The arrow is the site's one arrow, turned. `#i-arr` points up-right, so
     +45deg lands it pointing right and 225 lands it pointing left. */
  .vidx__nav .ico { transform: rotate(45deg); }
  .vidx__nav--prev .ico { transform: rotate(225deg); }

  @media (prefers-reduced-motion: reduce) {
    .vidx > .vid { transition: none; transform: none; opacity: 1; }
    .vidx__dot.on .vidx__fill { animation: none; }
  }

  /* The stack is empty once the frames have moved into the track. `initVidx`
     sets the class; `:empty` was tried first and never matched, because indented
     markup leaves whitespace text nodes behind and `:empty` counts those. */
  .bench__grid--x .bench__side { display: none; }

  .bench { margin-top: clamp(44px, 10vw, 68px); }
  .bench__grid {
    /* Named once and used twice: the gap between blocks, and the amount the
       transport has to CANCEL out of it (below). Two rules that have to agree
       about a number should not each be given their own copy of it. */
    --bench-gap: clamp(28px, 7vw, 40px);
    margin-top: clamp(24px, 6vw, 30px);
    gap: var(--bench-gap);
  }

  /* THE BENCH GOES TO ONE COLUMN WHENEVER THE CAROUSEL IS UP, AND THE 760–899
     BAND IS WHY THIS RULE EXISTS.

     `.bench__grid` has its own `@media (min-width: 760px)` two-column layout —
     feature left, stack right, subscribe under the feature — which overlaps the
     carousel's 899px breakpoint by 140px. In that band the track was being laid
     out inside a 1.62fr column while carrying `margin-inline: calc(var(--gut) *
     -1)` to break full-bleed out of the CONTENT column, so it burst its cell and
     took the transport's buttons 17px past the right edge of the document.
     Symptom: an 800px-tall probe at 768 coming back `scrollWidth 785`.

     The carousel is a single-column component. Rather than teach it about a
     layout it replaces, the layout is switched off while it is mounted — which
     is exactly what the `--x` class is for, and it is set only when the JS has
     actually built the track. */
  .bench__grid--x {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto;
  }

  /* =========================================================================
     SUBSCRIBE STAYS A SIGN, NOT A CONTROL — REVERTED, AT THE OWNER'S CALL.

     It was rebuilt here as a full-width bordered row with the YouTube mark on
     it, on the argument that the desktop's "no panel, just type" note is written
     entirely about the hole the feature frame leaves in a two-column bench, and
     that a large centred word with no edge does not say it can be pressed. The
     owner looked at both and kept the bare one, and he is right for a reason the
     argument missed: the bench above it is now a carousel — one framed screen
     with a bordered transport under it — so a bordered plate directly beneath
     was a third rectangle in a stack of rectangles, which is the exact fault
     `.subx`'s original note was written to avoid. Fewer boxes, not more.

     SO ALL THAT IS LEFT HERE IS SPACING, AND THE RULE GOES TOO. The `border-top`
     from the first pass was doing the job the gap now does on its own: after the
     transport strip — which already ends the carousel with a line of controls —
     a hairline read as a second divider ~30px under the first.
  ========================================================================= */
  .subx {
    /* Grid gap does the separating; a margin on a grid item stacks on top of it
       and that is how these two ended up 80px apart when both wanted ~30. */
    margin-top: 0;
    padding-block: clamp(18px, 4vw, 26px) clamp(4px, 1vw, 8px);
    border-top: 0;
  }
}

/* -----------------------------------------------------------------------------
   THE PLATFORM ROWS WERE MISALIGNED, AND THE TAP-TARGET PASS DID IT.

   `.plat__b` is built as a two-ROW grid: the brand icon spans both rows in
   column 1, the name sits in column 2 row 1, its label in column 2 row 2, and
   the arrow holds column 3. Every cell is placed by hand, and the comment on it
   in sections.css explains why.

   The site's `(pointer: coarse)` block then lists `.plat__b` among the row links
   that get `display: inline-flex` for their tap target — which throws the grid
   away entirely. All four children collapse onto one line in source order, so
   the label starts wherever the brand name happened to end and the arrow starts
   wherever the label did: measured at 390, the three labels began at 142 / 177 /
   150 and the three arrows at 274 / 282 / 290. Nothing lined up with the row
   above or below it, which is exactly what the owner was looking at.

   `.plat__b` never needed the display change: it is a padded block already 55px
   tall, well past the 38px floor, so the `min-height` alone was always doing the
   whole job. Restoring `display: grid` puts the placed cells back.

   Scoped `(pointer: coarse)` rather than by width, because that is the query
   that broke it — the fault appears on a tablet at 1024px too, where a
   `max-width: 899px` fix would never fire.
----------------------------------------------------------------------------- */
@media (pointer: coarse) {
  .plat__b { display: grid; }
}

/* -----------------------------------------------------------------------------
   THE STATEMENT COMES OFF THE PHONE.

   `.say` is the sleeve note between the last channel and the footer — a pull
   quote on the site's amber cue mark, then three indexed columns saying what
   each role actually does. On a wide screen it is the beat that closes the rack.

   On a phone it is 0.82 of a screen, arriving after fifteen screens of the same
   argument made concretely: the roles it names (Producer / Engineer / Developer)
   are the four words in the hero legend, and the claim it makes is what the
   Studio channel spends three paragraphs on. It is the one block on the page
   that restates rather than adds, and a phone reader has already made the
   scroll-or-leave decision several screens earlier.

   `display: none`, not removed — the desktop statement is untouched.
----------------------------------------------------------------------------- */
@media (max-width: 899px) {
  main > .say { display: none; }
}

/* =============================================================================
   CH 06 — PROCESS
============================================================================= */
@media (max-width: 899px) {
  /* ---------------------------------------------------------------------------
     THE NUMBER JOINS THE TITLE INSTEAD OF SITTING ON A SHELF ABOVE IT.

     Each stage is a 32px numbered chip, then 20px of margin, then the title,
     then the copy — a column that works across four cards side by side, where
     the chips line up in a row and read as the rack's index. Stacked, that row
     no longer exists: each chip is alone above its own heading with 20px of
     nothing under it, four times, and the section spends ~200px on the gaps
     between a number and the word it numbers.

     Inline, "01 Talk" is one heading with a marker, which is what it always
     meant. The copy is aligned under the TITLE rather than under the chip so
     the stage still has ONE left edge (rule M1) and the number hangs in the
     margin the way a list marker should.
  --------------------------------------------------------------------------- */
  .proc__step {
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 14px;
    row-gap: 8px;
    align-items: center;
  }
  .proc__n { margin-bottom: 0; }
  .proc__d { grid-column: 2; }
}

/* =============================================================================
   CH 07 — CONTACT
============================================================================= */
@media (max-width: 899px) {
  /* ---------------------------------------------------------------------------
     THE STATE LINE LOSES ITS SEPARATORS RATHER THAN ITS SHAPE.

     Three facts joined by drawn dots. The dots were drawn precisely so that a
     wrapped line could not start on a typed middot — but a drawn dot is still a
     flex ITEM, and on a phone the line breaks after one, so it ended
     "…IST 22:12:43 ·" with the last fact alone underneath. The construction
     that was meant to survive a wrap was defeated by the wrap it was built for.

     At this width there is no separator that can be safe, because the wrap point
     moves with the clock's own digits. So the dots go and the SPACE does the
     separating: an 18px column gap is unambiguous between three short mono
     phrases, and nothing can be stranded because there is nothing between them
     to strand. Measured: "Available for work" and the clock share the first row
     at 320 and 390, and "Replies within a day" takes the second.
  --------------------------------------------------------------------------- */
  .patch__state { gap: 8px 18px; }
  .patch__dot { display: none; }

  /* A PRIMARY ACTION SPANS THE COLUMN (rule M3). "Email me" is the one thing
     this channel is asking for and it was a 270px button in a 338px column —
     wide enough to look like a button, narrow enough to look like it was not
     sure. Matched to the hero's pair, which is the other place the site asks
     for the same thing. */
  .patch__act .btn {
    display: flex;
    width: 100%;
    min-height: 54px;
  }
}

/* =============================================================================
   THE FOOTER — a sign-off, and only a sign-off.

   Cut to the owner's brief: no mark, no back-to-top, no clock, no availability
   strip, no cities, and the closing note on ONE line. What is left is the four
   things a footer has to carry — who this is, what he does, where else to find
   him, and the legal/doc links that have no other home.

   ALL OF IT IS PHONE-ONLY. The desktop footer is a site map with four link
   columns and it was explicitly restored once after being redesigned without
   being asked; nothing in this block can reach it.

   WHY EACH ONE GOES, RATHER THAN JUST "ASKED FOR":
   - The availability lamp and the four cities are BOTH said elsewhere — the
     lamp and "Available for work" in CH 07's state line, the cities in the
     menu's own sign-off. On a phone the footer sits two screens under the
     contact channel, so it was repeating a fact the reader had just been given.
   - The clock is the same readout as the one in the contact channel.
   - The mark is the third identity statement in a 200px column (mark, wordmark,
     tagline), and it is also the nav's logo, on screen the entire time.
   - Back to top is a duplicate of a gesture every phone already has, and it was
     the only reason the end block needed two rows.
============================================================================= */
@media (max-width: 899px) {
  /* The availability line, the cities and the mark. `display: none`, never
     deleted from the markup — every one of them is the desktop footer's, and the
     breakpoint is what decides. */
  .foot__head,
  .foot__logo { display: none; }

  /* ---------------------------------------------------------------------------
     BACK TO TOP COMES BACK, at the owner's request, and the earlier reasoning
     for cutting it is worth correcting rather than deleting.

     It was removed on the grounds that it duplicates a gesture every phone
     already has. That is true of iOS — a tap on the status bar — and it is not
     true anywhere else, it is invisible to anyone who has not been told, and it
     is the wrong test besides: this page is fifteen screens on a phone, and the
     footer is where somebody who has read all of it decides what to do next.
     The one thing a fifteen-screen page owes them at the bottom is a way back to
     the top of it that is visible.

     THE SAME OBJECT AS THE DESKTOP'S, which is the point of the ask — `#totop`
     is one element and one set of styles; all that changes here is where it sits
     and how big its target is. It keeps `.foot__top`'s muted mono and its
     underline (which a finger will never see, and which costs nothing to leave
     in place for anyone on a tablet with a pointer).

     PLACED ABOVE THE CLOCK, INSIDE THE ROW IT IS ALREADY IN. `.foot__end-r`
     holds both and is already lifted above the sign-off by `order: -1`; making
     it a column and ordering the link first gives Résumé/Terms/Privacy → Back to
     top → the time → rule → copyright. That is the block ordered by how much
     each line asks of the reader, which is the order a footer should close in.
  --------------------------------------------------------------------------- */
  .foot__end-r { flex-direction: column; gap: 0; }
  #totop {
    order: -1;
    display: inline-flex; align-items: center;
    /* A real tap target on a control whose text is 11px. Padding, never a font
       size — the console voice is 11.36px by design and the words were always
       readable; it was the HITBOX that was not. */
    min-height: 44px;
    padding-inline: var(--s-4);
  }
  /* The underline sits `bottom: -3px` off a text box that now has 16px of
     padding under it, which would leave the bar floating in the middle of the
     target. Pinned to the text instead. */
  #totop::after { bottom: 12px; left: var(--s-4); right: var(--s-4); }

  /* ---------------------------------------------------------------------------
     THE CLOCK COMES BACK, UNDER THE DOC LINKS, AS THE QUIETEST THING HERE.

     Asked for after it had been cut: the time is worth keeping, it just is not
     worth the row it used to share with a button. It is a READOUT — it tells you
     it is nearly midnight where he is, which changes when you expect a reply —
     so it is set below everything it qualifies and dimmed a step past the muted
     text around it, `--c-faint` at 80%. Nothing else on the page is that quiet,
     which is the point: it is information you may want, never information you
     are being shown.

     THE RULE MOVES OFF `.foot__end` AND ONTO THE NOTE so the clock lands ABOVE
     it. Reading order is then Résumé/Terms/Privacy → the time → rule →
     copyright, which is what "after resume, terms" asks for: the divider now
     separates the sign-off from everything else rather than the two meta lines
     from each other. `order` does the move, so the markup is untouched — the
     clock stays in `.foot__end-r` where the desktop needs it.
  --------------------------------------------------------------------------- */
  .foot__end {
    flex-direction: column;
    border-top: 0;
    padding-top: 0;
    gap: 0;
  }
  /* NO padding above the clock. It reads as the last line of the block it
     qualifies (Résumé · Terms · Privacy), not as the first line of a new one,
     so the only space it needs is beneath it, before the rule. The gap above it
     is paid for once by `.foot__grid`'s bottom padding and nowhere else — this
     used to inherit a `padding-block` further down the file too, which is what
     stacked 16px on top of the grid's 26 and left the widest gap in the footer
     sitting directly above its quietest line. */
  .foot__end-r { order: -1; padding-block: 0 var(--s-4); }
  .foot__end-r #clock {
    font-size: clamp(8.6px, 2.6vw, 11.36px);
    color: var(--c-faint);
    opacity: .8;
  }
  .foot__note {
    width: 100%;
    justify-content: center;
    padding-top: var(--s-4);
    border-top: 1px solid var(--c-line);
  }

  /* ---------------------------------------------------------------------------
     ONE RHYTHM DOWN THE IDENTITY BLOCK.

     It was carrying five gaps of 12 / 24 / 44 / 24 / 30, so the widest space in
     the footer sat between the tagline and the socials for no reason other than
     that two different rules had set it. A sign-off is one object, so everything
     in it is spaced on a single step.

     ASYMMETRIC BLOCK PADDING, and deliberately so. The top pays for the
     wordmark's air (it is the first thing in the footer and now the largest type
     in it); the bottom is nearly closed because what follows it is the clock,
     which belongs to the links above it rather than to the sign-off below.
  --------------------------------------------------------------------------- */
  .foot__grid { padding-block: clamp(30px, 6vw, 46px) 12px; }
  .foot__id { gap: 0; }
  /* BIGGER — twice, at the owner's request — and it can afford to be: with the
     `J` mark gone this is the only identity in the block and the first thing in
     it, so it carries the job the mark and the wordmark used to share.

     The original `clamp(1.45rem, 3.2vw, 2.1rem)` floored at 23px on EVERY phone:
     a 3.2vw curve does not reach its own 1.45rem floor until 725px, so the
     "fluid" size was a fixed one everywhere it actually mattered. On a real vw
     curve it is ~40px at 390 and still leaves ~40px of column beside it.

     Its own margins give it the air above and below rather than the block's
     padding doing it, so the wordmark can be re-sized without the tagline's gap
     having to be retuned to match. */
  .foot__mark {
    /* ASKED FOR TWICE OVER, and it is the right instinct: this is now the
       largest type in the footer and the first thing in it, so it is the block's
       masthead — and a masthead is defined as much by the space around it as by
       its size. It was carrying 36px above and 36 below, which is the same beat
       as every gap in the sign-off; a heading spaced like its own body copy
       reads as the first line of a paragraph.

       ITS OWN MARGINS, NOT THE BLOCK'S PADDING, deliberately — so the wordmark
       can be re-sized later without the tagline's gap having to be retuned to
       match it (the reason the margins were put here in the first place). */
    margin-top: 34px;
    margin-bottom: 38px;
    /* The FLOOR is the number that needed care, not the preferred size: at 320
       a 2.4rem floor overrode the 10.4vw curve and rendered the name 263px wide
       in a 268px column — five pixels from wrapping. 2.1rem sits just under the
       curve's own value there, so the vw term stays in charge at every width in
       range and the name keeps ~38px of slack at its narrowest. */
    font-size: clamp(2.1rem, 10.4vw, 3.4rem);
    line-height: 1.06;
  }
  /* THE TAGLINE COMES OFF THE PHONE FOOTER, at the owner's request, and it is
     the fourth thing in a row saying the same thing. "Software and records —
     designed, built and mixed end to end, by one person" is the Statement
     section's claim (hidden here), the hero legend's four words, and what CH 01
     spends an editorial on. In a sign-off whose whole job is name → where else →
     legal, it was the only line asking to be READ. */
  .foot__tag { display: none; }
  .foot__social { margin-top: 26px; }
  .foot__links { margin-top: 18px; }
}

/* ---------------------------------------------------------------------------
   THE CLOSING NOTE — "© 2026 Jesvi Jonathan · Jesvi.net", ONE LINE.

   HISTORY, because it explains why the name is back. The full string is
   "© 2026 Jesvi Jonathan · Designed & Built Sincerely · Jesvi.net", which is
   517px at 390 and 424 at 320 — on one line that needs a 7.6px and a 6.1px type
   size, past legible. So the name was temporarily dropped from the copyright to
   buy the room.

   With "Designed & Built Sincerely" itself now off the phone at the owner's
   request, THAT CONSTRAINT IS GONE and the name comes back: the remaining line
   is `24.78 × font-size` wide, so at 390 it is ~208px inside a 354px column and
   at 320 ~178 inside 284. It fits at any size in range with room to spare, and
   "© 2026 · Jesvi.net" with no name in it is a worse copyright notice than the
   two extra words cost. **If the craft line is ever restored here, `.foot__who`
   has to be hidden again — the two are one budget.**

   THE SIZE IS LEFT AT THE SMALL END DELIBERATELY. It was taken down twice on
   request and the fit now allows ~12px at 390, so there is headroom if it reads
   too quiet — but this is the last and least important line on the page, and
   nothing about it needing to be bigger has been asked for.

   `flex-wrap` is left ALONE on purpose. The halves keep their `nowrap`, so if
   any of this arithmetic is ever wrong the line breaks at the phrase boundary
   rather than widening the document, which is exactly what this note did at 320
   before the size was made fluid.
--------------------------------------------------------------------------- */
@media (max-width: 899px) {
  /* The craft line goes; the name it was crowding out comes back. */
  .foot__craft { display: none; }
  .foot__note {
    flex-direction: row;
    align-items: baseline;
    gap: 0 6px;
    font-size: clamp(7.2px, 2.15vw, 10px);
  }
  /* The separator carried the two halves when they shared a row and was hidden
     when they stacked. They share a row again, so it comes back. */
  .foot__dot { display: inline; }
}

/* -----------------------------------------------------------------------------
   ...AND ON THE NARROWEST PHONES THE LABEL GUTTER STOPS PAYING FOR ITSELF.

   An 88px gutter inside a 320px screen leaves ~126px for the value, which turns
   "Full-time roles & freelance builds · remote or Chennai" into four lines — the
   exact fault the original comment on `.ed__fact` warned a label column would
   cause, which was true of a 300px margin column and becomes true again once
   the panel is this narrow. Label above value gives the value the whole width
   back. The label is still the first thing on the row, so nothing about how it
   reads changes; it just stops competing for the same line.
----------------------------------------------------------------------------- */
@media (max-width: 360px) {
  .ed__fact {
    grid-template-columns: minmax(0, 1fr);
    row-gap: 3px;
  }
}
