/* ── deck-only CSS ────────────────────────────────────────────────────
   Linked by index.html and by NOTHING else. Layers narrowest-scope-last:
   style.css (shared by both kinds of page) → this file (the deck) or
   blogs/blog.css (posts) → a page's own inline <style>.

   ⚠ `scroll-snap-type: y mandatory` lives here, at the top. It must never
   move into style.css or blogs/blog.css — either would apply it to every
   post and destroy long-form scrolling. It is safe here precisely because
   a post never links this file.

   This used to be a 744-line inline <style> in index.html, which put ~1000
   lines of CSS and JS around ~290 lines of markup in one 56KB file. Split
   out for the same reason blog.css was: so the thing you are editing is
   the thing you opened. ─────────────────────────────────────────────── */
  /* ── deck-only layout. Shared tokens + terminal grammar live in
     style.css; everything below is unique to this page. ───────── */

  /* Mandatory snap belongs to the deck ONLY — never move this into
     style.css, it would wreck long-form scrolling on blog posts. */
  html { scroll-snap-type: y mandatory; }

  /* ── screens: one snap-stop per scroll ──────────── */
  .screen {
    min-height: 100vh;   /* fallback for pre-dvh browsers */
    min-height: 100dvh;
    display: flex;
    align-items: center;
    justify-content: center;
    scroll-snap-align: start;
    padding: 3rem 1.5rem;
  }
  .screen > .inner {
    width: 100%;
    max-width: 720px;
  }

  /* below-the-fold screens hold their output until scrolled into view.
     The .dither arm of this gate lives BELOW the passes — see the note
     there; putting it here silently does nothing. */
  .reveal .in { animation-play-state: paused; }
  .reveal.on .in { animation-play-state: running; }

  /* ── pixel-dither reveal ─────────────────────────
     A screen's text emerges from a field of blocks that coarsens away in
     three passes: solid → ▓ → ▒ → gone. It is three stacked checkerboard
     layers rather than one animated pattern, because a layer can only fade
     as a whole — staggering their exits is what produces the dissolve.
     steps() timing is the point: it reads as pixels dropping out, not as a
     soft crossfade. Total ~520ms, overlapping the .in print underneath. */
  .content { position: relative; }
  .dither {
    position: absolute;
    inset: -10px -14px;
    z-index: 2;
    pointer-events: none;
  }
  .dither i { position: absolute; inset: 0; display: block; }

  /* pass 1 — solid cover */
  .dither i:nth-child(1) {
    background: var(--bg);
    animation: ditherOut 200ms steps(2) both;
  }
  /* pass 2 — half the cells left, coarse grid */
  .dither i:nth-child(2) {
    background-image: repeating-conic-gradient(var(--bg) 0% 25%, transparent 0% 50%);
    background-size: 18px 18px;
    animation: ditherOut 240ms steps(3) 150ms both;
  }
  /* pass 3 — fine grid, accent-tinted: the last cells sparkle out.
     color-mix degrades to "no layer" on pre-2023 browsers, which just
     costs the sparkle — the reveal still completes. */
  .dither i:nth-child(3) {
    background-image: repeating-conic-gradient(color-mix(in srgb, var(--accent) 11%, var(--bg)) 0% 25%, transparent 0% 50%);
    background-size: 9px 9px;
    background-position: 4px 0;
    animation: ditherOut 280ms steps(4) 280ms both;
  }
  @keyframes ditherOut { from { opacity: 1; } to { opacity: 0; } }

  /* ⚠ This gate MUST come after the three passes above and MUST outrank
     them. Each pass sets the `animation:` shorthand, which resets
     animation-play-state to `running` — an earlier `.reveal .dither i`
     rule has equal specificity, loses, and every below-the-fold screen
     burns its dissolve on page load, so you scroll down to already-revealed
     text. `:not(.on)` buys the extra class that makes this stick. */
  .reveal:not(.on) .dither i { animation-play-state: paused; }

  /* A card's dither is driven by .live, not by the screen gate: it must be
     able to re-run every time that card comes round again. Killing
     `animation-name` (rather than pausing) is what allows the restart — a
     paused animation that already finished stays finished, so re-adding
     .live would dissolve nothing. Same specificity trick as above. */
  .card:not(.live) .dither i { animation-name: none; opacity: 1; }

  /* ── projects track ───────────────────────────────
     The three project man pages used to be three snap screens; they are now
     one screen with a horizontal track, so the whole deck is 3 scrolls
     (owner's cap, July 2026). Horizontal snap is safe to nest inside the
     deck's vertical mandatory snap — they act on different axes — but note
     this is `x` here and `y` on <html>; don't merge the two declarations.
     scrollbar-width/::-webkit-scrollbar hide the strip: the segments below
     are the position indicator. */
  /* `.blogtrack` is the same mechanism paging the post list on #blog — the
     list outgrew one snap screen once the 2020 archive posts landed. Only
     the list slides; the `$ ls ~/blogs` prompt above it stays put, because
     the command didn't change, just the page of output you're looking at. */
  .mantrack, .blogtrack {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;   /* a swipe past the end must not fire the browser's back gesture */
    scrollbar-width: none;
    -ms-overflow-style: none;
    outline: none;
  }
  .mantrack::-webkit-scrollbar, .blogtrack::-webkit-scrollbar { display: none; }
  .mantrack:focus-visible, .blogtrack:focus-visible {
    outline: 1px dotted var(--text-faint);
    outline-offset: 6px;
  }
  .card, .bpage {
    flex: 0 0 100%;
    min-width: 0;
    scroll-snap-align: start;
    position: relative;   /* anchors the card's own .dither */
  }
  /* the list already carries the gap under the prompt; a page is just a
     column of it, so it adds no margin of its own */
  .bpage > .output { margin-top: 0; }
  /* the card's first line is a `$ man …` prompt and sits at the top of the
     screen's stack, same as the old per-project screens did */
  .card > .prompt { margin-top: 0; }
  /* the screen-level dither bleeds 14px sideways; inside a card that bleed
     lands in the scroll axis and adds a sliver of dead scroll past the last
     card, so square it off here */
  .card > .dither { inset: -10px 0; }

  /* ── the one asymmetric screen ────────────────────
     On #projects the mac sits LEFT and the man page reads on the right.
     Done with transform rather than by changing `left`, so the swap rides
     the compositor as you scroll in instead of relaying out a fixed
     element. -580px is the exact mirror: the mac's left edge moves from
     calc(50% + 90px) to calc(50% - 490px), its 400px width included.
     Desktop only — at ≤1020px the mac docks bottom-centre and there is no
     left or right to swap. */
  @media (min-width: 1021px) {
    .mac { transition: transform 620ms var(--ease-out); }
    .mac.flip { transform: translateY(-50%) translateX(-580px); }
    #projects .inner { justify-content: flex-end; }
  }

  /* position indicator: three blocks + a `1/3` counter, in the man-page's
     own faint register. The active block's fill is also the auto-advance
     countdown — it drains over one interval, and freezes when the track is
     paused, so the bar explains the behaviour without any copy. */
  .tbar {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    margin-top: 1.6rem;
  }
  .segs { display: flex; gap: 4px; }
  .segs i {
    display: block;
    width: 26px;
    height: 3px;
    background: var(--surface);
    overflow: hidden;
  }
  .segs i b {
    display: block;
    height: 100%;
    width: 0;
    background: var(--accent);
  }
  /* At rest the current segment is simply full. The drain animation is
     added only while the timer is actually running (.running) and freezes
     with it (.paused) — otherwise an off-screen or paused track would show
     a countdown to an advance that isn't coming. */
  .segs i.on b { width: 100%; }
  .tbar.running .segs i.on b { animation: segFill var(--dwell, 7s) linear both; }
  .tbar.running.paused .segs i.on b { animation-play-state: paused; }
  .segs i.done b { width: 100%; opacity: 0.35; }
  @keyframes segFill { from { width: 0; } to { width: 100%; } }
  .tcount {
    color: var(--text-faint);
    font-size: 0.72rem;
    letter-spacing: 0.04em;
  }

  /* ── ambient pixel field ─────────────────────────
     Faint grid behind everything, masked to a soft centre and panned one
     cell per 40s so the page is never quite static. Fixed layer, transform
     only — no layout cost. z-index:-1 keeps it under the text. */
  body::before {
    content: "";
    position: fixed;
    inset: -40px;
    z-index: -1;
    pointer-events: none;
    background-image:
      linear-gradient(var(--grid-line) 1px, transparent 1px),
      linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
    background-size: 32px 32px;
    -webkit-mask-image: radial-gradient(ellipse at 50% 42%, #000 18%, transparent 76%);
    mask-image: radial-gradient(ellipse at 50% 42%, #000 18%, transparent 76%);
    animation: drift 40s linear infinite;
  }
  @keyframes drift { to { transform: translate3d(32px, 32px, 0); } }

  /* only the screen's FIRST prompt loses its top margin — any later command
     on the same screen needs its normal rhythm to read as separate.
     The `.dither +` arm is the same rule: the reveal overlay is .content's
     real first child, so the prompt is only first *visible* child. Don't
     collapse these into `.detail-screen .prompt` — that would zero the
     second command's margin too. */
  .screen .inner > section:first-child .prompt,
  .detail-screen .content > .prompt:first-child,
  .detail-screen .content > .dither + .prompt { margin-top: 0; }

  /* ── projects + blog index (same two-column list shape) ── */
  .projects { list-style: none; }

  .projects a {
    display: flex;
    align-items: baseline;
    text-decoration: none;
    color: inherit;
    padding: 0.35rem 0;
    transition: transform 160ms var(--ease-out);
  }
  .projects a:active { transform: scale(0.995); }

  /* Hover is the same dither vocabulary as the reveal: an accent pixel wash
     behind the row, and a block cursor stepping in ahead of the name — the
     underline it replaces was the one non-terminal gesture left on the page.
     The cursor grows inside the fixed-width .name column, so the desc
     doesn't shift. isolate + z-index:-1 puts the wash under the text
     without letting it escape the row. */
  @media (hover: hover) and (pointer: fine) {
    .projects a { position: relative; isolation: isolate; }
    .projects a::before {
      content: "";
      position: absolute;
      inset: -1px -8px;
      z-index: -1;
      background-image: repeating-conic-gradient(var(--accent) 0% 25%, transparent 0% 50%);
      background-size: 6px 6px;
      opacity: 0;
      transition: opacity 160ms steps(3);
    }
    .projects a:hover::before { opacity: 0.17; }
    .projects a:hover .desc { color: var(--text); }
    /* the description's bold fragment gets an accent block-underline that
       wipes across in steps — same pixel grammar as the reveal. It's a
       background, not text-decoration, so it can animate its width; a full
       block highlight was rejected because mid-sweep it puts light text on
       a solid accent and the contrast drops out. */
    .projects .desc b {
      background-image: linear-gradient(var(--accent), var(--accent));
      background-repeat: no-repeat;
      background-position: 0 100%;
      background-size: 0 2px;
      transition: background-size 220ms steps(6);
    }
    .projects a:hover .desc b { background-size: 100% 2px; }
    .projects .name::before {
      content: "";
      display: inline-block;
      width: 0;
      height: 0.95em;
      background: var(--accent);
      vertical-align: -0.14em;
      transition: width 140ms steps(3), margin-right 140ms steps(3);
    }
    .projects a:hover .name::before { width: 0.5em; margin-right: 0.3em; }
  }

  .projects .name {
    color: var(--accent);
    font-weight: 500;
    font-size: 0.85rem;
    width: 9.5rem;
    flex-shrink: 0;
  }
  .projects .desc {
    color: var(--text-dim);
    font-size: 0.8rem;
    transition: color 150ms ease;
  }
  .projects .desc b { color: var(--text); font-weight: 500; }
  /* post slugs run longer than project names — widen that column so they
     don't wrap mid-slug. Fits ~23 chars; keep slugs at or under that. */
  #blog .projects .name { width: 12.5rem; }

  /* date + read-time, the `ls -l` columns the listing was missing. It sits
     INSIDE .desc rather than beside it: `.projects a` is a two-item flex
     row (name | desc), so a third child would land in a third column and
     fight the desc for width. As a block inside the desc it simply drops
     under the line, and reflows with it on mobile. Scoped to #blog — the
     mac's pizow app already owns a `.meta`. */
  #blog .desc .meta {
    display: block;
    margin-top: 0.15rem;
    color: var(--text-faint);
    font-size: 0.72rem;
    letter-spacing: 0.02em;
  }
  #blog .desc .meta .sep { margin: 0 0.15rem; }

  @media (max-width: 540px) {
    .projects a { flex-direction: column; gap: 0.1rem; padding: 0.5rem 0; }
    .projects .name { width: auto; }
  }

  /* ── left column caps so text clears the fixed mac ─ */
  .home .inner { max-width: 940px; }
  .home .content,
  .detail-screen .content { max-width: 500px; }

  /* ── the mac: fixed on the right across all screens.
     Lid is hardcoded open; the app on screen follows the scroll. */
  .mac {
    position: fixed;
    top: 50%;
    left: calc(50% + 90px);
    transform: translateY(-50%);
    width: 400px;
    perspective: 1100px;
    z-index: 5;
    opacity: 0;
    animation: fadeIn 500ms var(--ease-out) 900ms both;
  }
  .mac .lid {
    position: relative;
    box-sizing: border-box;
    width: 360px;
    height: 232px;
    margin: 0 auto;
    border: 2px solid var(--text-faint);
    border-radius: 12px;
    background: var(--surface-soft);
    padding-top: 16px;
    transform-origin: bottom center;
    transform: rotateX(-84deg);
    transition: transform 900ms var(--ease-out);
    will-change: transform;
  }
  .mac.open .lid { transform: rotateX(0); }
  .mac .cam {
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: var(--text-faint);
    margin: 0 auto;
  }
  .mac .base {
    position: relative;
    width: 400px;
    height: 10px;
    border-radius: 5px;
    background: var(--text-faint);
  }
  .mac .notch {
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 44px;
    height: 4.5px;
    background: var(--bg);
    border-radius: 0 0 5px 5px;
  }

  /* apps on the mac screen — one per section, crossfade on scroll */
  .mac .app {
    position: absolute;
    left: 34px;
    right: 34px;
    top: 48px;
    font-size: 0.72rem;
    color: var(--text-dim);
    opacity: 0;
    transform: translateY(6px);
    transition: opacity 280ms var(--ease-out), transform 280ms var(--ease-out);
    pointer-events: none;
  }
  .mac[data-app="home"] .app-home,
  .mac[data-app="readback"] .app-readback,
  .mac[data-app="pizow"] .app-pizow,
  .mac[data-app="mac-mlx-cluster"] .app-mlx,
  .mac[data-app="blog"] .app-blog {
    opacity: 1;
    transform: none;
  }

  /* ── mac app swap: the same pixel wipe, one screen down ──
     The apps used to just crossfade, which was the last soft transition
     left on the page. Now the swap happens behind a block wipe: the cells
     cover the screen, then coarsen away over ~420ms while the incoming app
     fades in underneath. Finer cells than the page reveal (12px/6px vs
     18px/9px) because the lid is only 360px wide — the page's grid at this
     size reads as three big squares, not as pixels.

     At rest the layers carry no animation and sit at opacity 0; adding
     .swap applies the animations fresh, which is what makes it re-runnable
     on every change (a paused-and-finished animation never replays). This
     is NOT lid motion — the lid stays hardcoded open, as it must. */
  .mac .mdither {
    position: absolute;
    inset: 0;
    z-index: 3;
    border-radius: 10px;
    overflow: hidden;
    pointer-events: none;
  }
  .mac .mdither i {
    position: absolute;
    inset: 0;
    display: block;
    opacity: 0;
    animation-name: none;
  }
  .mac.swap .mdither i:nth-child(1) {
    background: var(--bg);
    animation: ditherOut 160ms steps(2) both;
  }
  .mac.swap .mdither i:nth-child(2) {
    background-image: repeating-conic-gradient(var(--bg) 0% 25%, transparent 0% 50%);
    background-size: 12px 12px;
    animation: ditherOut 200ms steps(3) 110ms both;
  }
  .mac.swap .mdither i:nth-child(3) {
    background-image: repeating-conic-gradient(color-mix(in srgb, var(--accent) 14%, var(--bg)) 0% 25%, transparent 0% 50%);
    background-size: 6px 6px;
    background-position: 3px 0;
    animation: ditherOut 220ms steps(4) 200ms both;
  }

  /* home app: terminal lines print in after the lid opens */
  .mac .tline {
    height: 5px;
    border-radius: 2.5px;
    margin-bottom: 12px;
    width: var(--w, 60%);
    background: var(--c, var(--text-dim));
    opacity: 0;
    transition: opacity 250ms ease;
  }
  .mac .tline.prompt-line {
    background: linear-gradient(90deg, var(--green) 0 16px, transparent 16px 24px, var(--text-dim) 24px);
  }
  .mac .dcursor {
    width: 8px;
    height: 14px;
    background: var(--accent);
    opacity: 0;
    transition: opacity 250ms ease;
    animation: blink 1.1s steps(1) infinite;
  }
  .mac.open[data-app="home"] .tline,
  .mac.open[data-app="home"] .dcursor {
    opacity: 1;
    transition-delay: calc(450ms + var(--n, 0) * 100ms);
  }
  /* mobile: the mac docks at the bottom of the viewport, scaled down —
     but only on the project screens; home stays a clean terminal. */
  @media (max-width: 1020px) {
    .mac {
      top: auto;
      bottom: calc(38px + env(safe-area-inset-bottom));  /* clears the tmux bar */
      left: 50%;
      transform: translateX(-50%) scale(0.75);
      transform-origin: bottom center;
      animation: none;
      opacity: 1;
      transition: opacity 300ms var(--ease-out);
    }
    .mac[data-app="home"] { opacity: 0; }
    .detail-screen { padding-bottom: 240px; }
  }
  @media (max-width: 440px) {
    .mac { transform: translateX(-50%) scale(0.68); }
    .detail-screen { padding-bottom: 220px; }
  }

  /* ── mobile: tighter spacing so each screen fits one viewport
     (bottom was clipping on Android) ─────────────────────────── */
  @media (max-width: 540px) {
    .screen { padding: 2rem 1.25rem calc(2rem + env(safe-area-inset-bottom)); }
    /* keep detail text clear of the docked mac (don't let the
       shorthand above clobber the padding-bottom set earlier) */
    .detail-screen { padding-bottom: calc(240px + env(safe-area-inset-bottom)); }
    .hint { margin-top: 1.9rem; }
  }

  /* short mobile viewports: a man page + the full-size docked mac don't
     both fit, and the mac was drawing over the content. NOTE: iOS Safari
     with scroll-snap rarely collapses its URL bar, so most iPhones live
     at ~620-760px viewport height permanently — the mac must still show
     there, just smaller. Only truly tiny viewports lose it. */
  @media (max-width: 1020px) and (max-height: 760px) {
    .mac {
      transform: translateX(-50%) scale(0.5);
      bottom: calc(30px + env(safe-area-inset-bottom));
    }
    .detail-screen { padding-bottom: calc(160px + env(safe-area-inset-bottom)); }
  }
  @media (max-width: 1020px) and (max-height: 660px) {
    .mac { opacity: 0; }
    .detail-screen { padding-bottom: calc(3rem + env(safe-area-inset-bottom)); }
  }

  /* ── project detail screens ─────────────────────── */
  .detail-screen .inner {
    max-width: 940px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 3.5rem;
  }
  .detail-screen .content { flex: 1; min-width: 0; }

  /* ── live demos (rendered inside the mac screen) ── */
  .pane-title {
    color: var(--text-faint);
    font-size: 0.7rem;
    margin-bottom: 0.9rem;
  }
  .pane-title b { color: var(--text-dim); font-weight: 500; }

  /* readback: playing */
  .wave {
    display: flex;
    align-items: flex-end;
    gap: 3px;
    height: 42px;
    margin-bottom: 1rem;
  }
  .wave i {
    flex: 1;
    height: var(--h, 60%);
    background: var(--accent);
    opacity: 0.7;
    border-radius: 2px;
    transform-origin: bottom;
    animation: eq var(--t, 1s) ease-in-out var(--d, 0s) infinite alternate;
  }
  @keyframes eq {
    from { transform: scaleY(0.2); }
    to   { transform: scaleY(1); }
  }
  .pbar {
    height: 4px;
    background: var(--surface);
    border-radius: 2px;
    overflow: hidden;
  }
  .pbar i {
    display: block;
    height: 100%;
    width: 37%;
    background: var(--accent);
    animation: fill 45s linear infinite;
  }
  @keyframes fill { from { width: 0; } to { width: 100%; } }
  .ptime {
    display: flex;
    justify-content: space-between;
    margin-top: 0.3rem;
    color: var(--text-faint);
    font-size: 0.68rem;
  }
  .track {
    margin-top: 0.7rem;
    color: var(--text-dim);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .track .arrow { color: var(--green); margin-right: 0.4rem; }

  /* pizow: monitor */
  .gauge {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin-top: 0.55rem;
  }
  .gauge .lbl { width: 2.8rem; color: var(--text-faint); }
  .gauge .bar {
    flex: 1;
    height: 5px;
    background: var(--surface);
    border-radius: 3px;
    overflow: hidden;
  }
  .gauge .bar i {
    display: block;
    height: 100%;
    width: var(--lo, 30%);
    background: var(--green);
    animation: load var(--t, 3s) ease-in-out infinite alternate;
  }
  .gauge .bar i.hot { background: var(--accent); }
  @keyframes load {
    from { width: var(--lo, 30%); }
    to   { width: var(--hi, 75%); }
  }
  .mac .meta {
    margin-top: 0.9rem;
    color: var(--text-faint);
    font-size: 0.68rem;
  }

  /* mac-mlx-cluster: serving */
  .kv { display: flex; gap: 0.8rem; margin-top: 0.4rem; }
  .kv .k { width: 3.2rem; color: var(--text-faint); }
  .type {
    margin-top: 0.9rem;
    color: var(--text-dim);
    white-space: nowrap;
    overflow: hidden;
    width: 24ch;
    border-right: 0.45em solid var(--accent);
    animation: typing 7s steps(24, end) infinite, caret 1.1s steps(1) infinite;
  }
  @keyframes typing {
    0%   { width: 0; }
    55%  { width: 24ch; }
    100% { width: 24ch; }
  }
  @keyframes caret { 50% { border-color: transparent; } }

  /* blog: a less(1) pager — text scrolls, status line counts up */
  .app-blog .pager {
    height: 86px;
    overflow: hidden;
    position: relative;
  }
  .app-blog .ptext { animation: pageScroll 11s ease-in-out infinite; }
  .app-blog .bline {
    height: 5px;
    border-radius: 2.5px;
    margin-bottom: 11px;
    width: var(--w, 60%);
    background: var(--text-dim);
    opacity: 0.55;
  }
  .app-blog .bline.h { background: var(--accent); opacity: 0.9; height: 7px; }
  @keyframes pageScroll {
    0%,   18%  { transform: translateY(0); }
    32%,  50%  { transform: translateY(-32px); }
    64%,  82%  { transform: translateY(-64px); }
    100%       { transform: translateY(0); }
  }
  /* less's inverted status line at the foot of the pager */
  .app-blog .lstatus {
    margin-top: 0.5rem;
    background: var(--surface);
    color: var(--text-faint);
    font-size: 0.68rem;
    padding: 1px 5px;
    white-space: nowrap;
    overflow: hidden;
  }
  .app-blog .pcts {
    display: block;
    animation: pagePct 11s steps(1) infinite;
  }
  .app-blog .pcts span { display: block; height: 1.5em; color: var(--accent); }

  .cycle {
    display: inline-block;
    height: 1.5em;
    line-height: 1.5em;
    overflow: hidden;
    vertical-align: bottom;
  }
  @keyframes pagePct {
    0%,   18%  { transform: translateY(0); }
    32%,  50%  { transform: translateY(-1.5em); }
    64%,  82%  { transform: translateY(-3em); }
    100%       { transform: translateY(0); }
  }

  @media (prefers-reduced-motion: reduce) {
    .type { width: 24ch; }
    .hint .down { animation: none; }
    /* keep `both` fill so the layers still end up cleared, just instantly */
    .dither i { animation-duration: 1ms !important; animation-delay: 0s !important; }
    body::before { animation: none; }
    /* auto-advance is off in this mode, so the countdown fill would be
       lying about what happens next — show the segment simply as current */
    .segs i.on b { animation: none; width: 100%; }
    .mac,
    .mac * { animation: none !important; transition: none !important; }
    .mac .lid { transform: none !important; }
    .mac .tline, .mac .dcursor { opacity: 1 !important; }
  }

  /* ── scroll hint ──────────────────────────────────
     Was --text-faint at 0.8rem: the dimmest token at the smallest size, and
     it disappeared against the background. It reads as a shell comment, so
     the `#` keeps the faint colour while the sentence itself sits at
     --text-dim / 0.85rem like every other output line.
     The arrow is the page's only "do this" cue — accent-coloured, and it
     travels down and fades out on a loop instead of bobbing in place, so it
     points somewhere rather than just wobbling. */
  .hint {
    margin-top: 2.5rem;
    color: var(--text-dim);
    font-size: 0.85rem;
  }
  .hint .c { color: var(--text-faint); }
  .hint .down {
    display: inline-block;
    color: var(--accent);
    animation: drip 2.2s var(--ease-out) infinite;
  }
  /* Never fades fully out: the arrow sits mid-sentence, and a glyph that
     vanishes reads as a rendering glitch rather than a cue. It bottoms out
     at 0.18 and snaps back up on that same dim frame, so the reset is
     invisible without leaving a hole in the line. */
  @keyframes drip {
    0%   { transform: translateY(-1px); opacity: 0.4; }
    30%  { transform: translateY(3px);  opacity: 1; }
    62%  { transform: translateY(9px);  opacity: 0.18; }
    63%  { transform: translateY(-1px); opacity: 0.18; }
    100% { transform: translateY(-1px); opacity: 0.4; }
  }

  /* ── short viewports (laptops) ──────────────────────
     The home stack is ~760px tall; on shorter screens the flex-centering
     leaves no bottom margin, so the reading/socials/brew cluster crowds the
     bottom edge (or falls below the fold). Tighten the rhythm as the
     viewport shrinks — tall screens keep the roomy 2.5rem spacing. */
  @media (max-height: 840px) {
    .screen { padding-top: 2rem; }
    /* ⚠ padding-bottom is deliberately NOT set on .detail-screen here.
       The mac-clearance tiers above (240/220/160px) are `.detail-screen`,
       a single class — same specificity as `.screen` — so a
       `.screen { padding-bottom }` this far down the file silently wins and
       the docked mac draws over the man pages. Measured before this was
       scoped: at a 673px viewport the padding collapsed to 2rem (27px, on
       the 13.5px root font) and #projects overlapped the mac by 17px.
       Same trap the `max-width: 540px` block above already documents. */
    .screen:not(.detail-screen) { padding-bottom: 2rem; }
    .prompt, footer, .hint { margin-top: 1.7rem; }
    .output { margin-top: 0.6rem; }
    .projects a { padding: 0.25rem 0; }
  }
  @media (max-height: 730px) {
    .prompt, footer, .hint { margin-top: 1.2rem; }
    .projects a { padding: 0.15rem 0; }
  }
