  :root {
    /* Fluid type. Everything was pinned to fixed pixels tuned to the shortest
       window that had to fit, which left a dead band under the content on a
       taller one and made the whole page read small. These grow with the
       viewport height instead: the floor is what a 756px-tall window gets, the
       ceiling stops a large monitor looking cartoonish. Vertical rhythm uses
       the same terms so the layout keeps its proportions as it scales.
       The vh coefficients are deliberately gentle: line wrapping is bound by
       width, not height, so type that grows in step with the height makes the
       content taller than the window gains. */
    --fs-body:  clamp(15.5px, calc(0.95vh / var(--zoom) + 8.3px), 19px);
    --fs-lead:  clamp(16.5px, calc(1.05vh / var(--zoom) + 8.6px), 21px);
    --fs-ui:    clamp(14px, calc(0.80vh / var(--zoom) + 7.9px), 17px);
    --fs-micro: clamp(12px, calc(0.65vh / var(--zoom) + 7.1px), 15px);
    --fs-head:  clamp(17.5px, calc(1.00vh / var(--zoom) + 9.9px), 22px);
    --fs-hero:  clamp(28px, calc(1.90vh / var(--zoom) + 13.6px), 38px);
    --pad-screen: clamp(10px, calc(1.3vh / var(--zoom)), 22px);

    /* Sidhant works with the browser at 80% and wants that to be what everyone
       gets, so the desktop rule below zooms the terminal to match. Phones keep
       1: the layout is already narrow there and 0.8 would just make it small. */
    --zoom: 1;

    --bg: #0a0a0f;
    --fg: #e0e0e0;
    --muted: #7a8a82;
    --dim: #4a6a5d;
    --accent: #00e5a0;
    --warn: #ffbd2e;
    --danger: #ff5f56;
    --linkedin: #0a66c2;
    --border: #1a3a2e;
    --panel: #0d0d13;
    --header: #17171f;
  }

  * { box-sizing: border-box; }

  html, body {
    margin: 0; padding: 0;
    background: #1a1a22;
    min-height: 100vh;
    font-family: 'SF Mono', 'JetBrains Mono', 'Menlo', 'Consolas', ui-monospace, monospace;
    font-size: var(--fs-body);
    color: var(--fg);
  }

  body {
    display: flex;
    align-items: stretch;
    justify-content: center;
    padding: 20px 16px;
    min-height: 100vh;
  }

  .terminal {
    position: relative;
    z-index: 1;
    width: 100%;
    max-width: 1600px;
    background: var(--bg);
    border-radius: 8px;
    overflow: hidden;
    border: 1px solid #2a2a35;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
    display: flex;
    flex-direction: column;
    min-height: calc(100vh - 40px);
    /* 100vh includes the mobile URL bar, so the window overshoots the screen
       and the page scrolls by the height of the bar with nothing in it. */
    min-height: calc(100dvh - 40px);
  }

  .terminal::before {
    display: none;
  }

  .title-bar {
    background: var(--header);
    padding: 9px 14px;
    display: flex;
    align-items: center;
    gap: 8px;
    border-bottom: 1px solid #23232e;
    position: relative;
    z-index: 2;
    flex-shrink: 0;
  }
  .dot {
    width: 12px; height: 12px;
    border-radius: 50%;
    padding: 0;
    border: none;
    flex-shrink: 0;
  }
  button.dot { cursor: pointer; }
  button.dot:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  .dot.red { background: var(--danger); }
  .dot.yellow { background: var(--warn); }
  .dot.green { background: #27c93f; }
  .title-text {
    flex: 1;
    text-align: center;
    font-size: 15px;
    color: #8a8a9a;
    user-select: none;
  }

  .screen {
    padding: var(--pad-screen) clamp(20px, calc(1.8vw / var(--zoom)), 40px);
    display: flex;
    flex-direction: column;
    position: relative;
    z-index: 2;
    flex: 1;
  }

  .boot-screen {
    padding: 24px 28px;
    font-size: 15px;
    line-height: 1.7;
    color: var(--accent);
    flex: 1;
  }
  .boot-screen .ok { color: var(--muted); }
  .boot-screen .dim { color: #8a9a92; }
  .boot-screen .cmd { color: var(--fg); }

  @keyframes blink { 50% { opacity: 0; } }

  .motd {
    color: var(--dim);
    font-size: 15px;
    font-style: italic;
    margin-bottom: 8px;
    padding-left: 12px;
    border-left: 2px solid var(--border);
    line-height: 1.6;
    /* One line, always. The 760px cap was what pushed the longer quotes onto a
       second line. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .motd .quote-by { color: var(--muted); font-style: normal; }

  .main-grid {
    display: grid;
    grid-template-columns: minmax(300px, 340px) 1fr;
    gap: 40px;
    flex: 1;
  }

  .portrait-col { display: flex; flex-direction: column; gap: 16px; min-height: 0; }

  /* Real photo portrait — replaces ASCII */
  .photo-portrait {
    width: 100%;
    max-width: 310px;
    aspect-ratio: 500 / 489;
    object-fit: cover;
    border: 1px solid var(--border);
    border-radius: 4px;
    filter: grayscale(0.15) contrast(1.05);
    display: block;
    background: #111;
  }

  .sysinfo {
    flex: 0 0 auto;
    font-size: 14px;
    line-height: 1.6;
    color: var(--fg);
    padding: 14px 18px;
    border: 1px solid var(--border);
    border-radius: 3px;
    background: rgba(0, 229, 160, 0.02);
    max-width: 310px;
  }
  .sysinfo .k {
    color: var(--accent);
    display: inline-block;
    min-width: 75px;
    padding-right: 14px;
  }
  .sysinfo .v { color: var(--fg); }
  .sysinfo .divider { color: var(--dim); letter-spacing: 1px; margin: 5px 0; overflow: hidden; }
  .sysinfo .color-bar { display: flex; gap: 2px; margin-top: 8px; }
  .sysinfo .color-bar span { width: 15px; height: 11px; display: inline-block; }

  .content-col {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
  }

  .brand {
    color: var(--accent);
    font-size: 13px;
    line-height: 1;
    white-space: pre;
    margin-bottom: 20px;
    user-select: none;
  }

  /* The ASCII wordmark carries the name visually, so it is the real heading
     only in appearance. This is the actual <h1> for screen readers and search,
     and below 640px it becomes the visible one — 63 characters of ASCII cannot
     shrink to a phone width and stay readable. */
  .brand-text {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: 0;
    overflow: hidden;
    white-space: nowrap;
    clip-path: inset(50%);
    font-size: 1rem;
  }

  .intro {
    color: var(--fg);
    line-height: 1.5;
    font-size: 16px;
    max-width: none;
  }
  .intro p { margin: 0 0 11px; }
  .intro p.dim { color: var(--muted); }
  .intro .accent { color: var(--accent); }

  /* ===== Resume / connect buttons ===== */
  .cta-row {
    margin-top: 12px;
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
  }
  .cta-btn {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 10px 20px;
    font-family: inherit;
    font-size: 15px;
    color: var(--accent);
    background: transparent;
    border: 1px solid var(--border);
    border-radius: 3px;
    text-decoration: none;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
  }
  .cta-btn:hover {
    background: rgba(0, 229, 160, 0.1);
    border-color: var(--accent);
  }
  .cta-btn.primary {
    background: rgba(0, 229, 160, 0.08);
    border-color: rgba(0, 229, 160, 0.4);
  }
  .cta-btn.primary:hover {
    background: rgba(0, 229, 160, 0.18);
  }
  .cta-btn .arrow { color: var(--muted); }
  .cta-btn:hover .arrow { color: var(--accent); }


  .quick-stats {
    margin-top: 12px;
    max-width: none;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
  }
  .stat-card {
    padding: 10px 16px;
    border: 1px solid var(--border);
    border-radius: 3px;
    background: rgba(0, 229, 160, 0.02);
  }
  .stat-card .num {
    color: var(--accent);
    font-size: 32px;
    font-weight: 500;
    line-height: 1.1;
    display: block;
  }
  .stat-card .lbl {
    color: var(--muted);
    font-size: 14px;
    text-transform: uppercase;
    letter-spacing: 1px;
    margin-top: 6px;
  }

  /* "currently building" strip — the mark sits dim until you look at it.
     flex: 1 so it takes whatever the content column has left and its bottom
     edge lands level with the sysinfo panel in the portrait column beside it,
     rather than stopping short and leaving a gap above the band. */
  /* The mark and the three lines are sized against the strip's own height, not
     the viewport: the strip takes whatever the content column has left, which
     is not a fixed share of the window, so a vh clamp cannot track it. It grew
     when the shell prompt above it went away, and the contents stayed put and
     floated in the middle of it. container-type: size makes 100cqh the strip's
     height so they can follow it.

     Containment means the strip reports no content height of its own, so
     min-height is the floor that stops it collapsing to nothing when the column
     has no room to give - it is the same three lines and the same padding, at
     the sizes the clamps below bottom out at. */
  .now-building {
    flex: 1 1 auto;
    container-type: size;
    min-height: calc(1.35 * (var(--fs-micro) + var(--fs-lead) + var(--fs-body)) + 36px);
    margin-top: 10px;
    max-width: none;
    display: flex;
    align-items: center;
    gap: clamp(14px, calc(1.4vw / var(--zoom)), 26px);
    padding: clamp(8px, calc(1vh / var(--zoom)), 16px) clamp(12px, calc(1.2vw / var(--zoom)), 22px);
    border: 1px solid var(--border);
    border-radius: 4px;
    background: rgba(0, 229, 160, 0.02);
    text-align: left;
    color: inherit;
    width: 100%;
  }
  .now-building .mark {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    border-right: 1px solid var(--border);
    padding-right: clamp(12px, calc(1.2vw / var(--zoom)), 22px);
  }
  /* Height-driven with width: auto, so the moth keeps its own 512x218 and the
     rule does not have to restate it. */
  .now-building .mark img {
    width: auto;
    height: clamp(34px, 56cqh, 104px);
    opacity: 0.75;
    filter: saturate(0);
  }
  .now-building .nb-label {
    display: block;
    font-size: clamp(var(--fs-micro), 14cqh, 20px);
    line-height: 1.35;
    text-transform: uppercase;
    letter-spacing: 1.5px;
    color: var(--muted);
    margin-bottom: 4px;
  }
  .now-building .nb-name {
    display: block;
    font-size: clamp(var(--fs-lead), 23cqh, 32px);
    line-height: 1.35;
    color: var(--fg);
    letter-spacing: 0.5px;
  }
  .now-building .nb-name .tagline {
    color: var(--accent);
    font-size: 0.76em;
    margin-left: 10px;
  }
  .now-building .nb-desc {
    display: block;
    font-size: clamp(var(--fs-body), 17cqh, 24px);
    line-height: 1.35;
    color: var(--muted);
    margin-top: 3px;
  }


  /* ===== home band: chess, photography, on repeat =====
     The main grid used to carry flex: 1 and swallowed the whole lower half of
     the home screen as dead space. It sizes to its content now and the band
     takes what is left, so the section still fills the window exactly.

     Two of the three cards are a square with a couple of lines under it, and a
     square in a band this shape has to take its size from the band's HEIGHT,
     which is the bound - a square sized from the column's width would be twice
     as tall as the band. CSS cannot do that through intrinsic sizing: a flex
     item's width is resolved before the line's height is known, so a column
     asked to be as wide as the board inside it turns out 2px wide. Container
     query units can, hence `container-type: size` on the band and `100cqh`
     below - "as wide as the band is tall, less the lines of text". Each of
     those columns keeps a plain clamp() first as the fallback for anything
     without container queries.

     Size containment also means the band reports no content height of its own,
     which is what keeps a 1100px photo from dragging the whole section past
     the bottom of the window. Both are undone under 821px, where the band
     stacks and flows like ordinary content. */
  #home .main-grid { flex: 0 0 auto; }
  .home-band {
    flex: 1;
    min-height: 0;
    container-type: size;
    display: flex;
    align-items: stretch;
    gap: clamp(16px, calc(1.9vw / var(--zoom)), 38px);
    margin-top: clamp(8px, calc(1.2vh / var(--zoom)), 16px);
    /* The board and the cover used to sit hard against the edges of the screen
       while the photo floated in a wide middle. This walks all three inwards.
       It eats into cqw, so the width ceilings below bind a little sooner, which
       is the safe direction. */
    padding-inline: clamp(0px, calc(5vw / var(--zoom)), 112px);
  }
  .band-card {
    min-height: 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    text-decoration: none;
    color: inherit;
  }
  a.band-card:focus-visible { outline: 1px solid var(--accent); outline-offset: 4px; }

  /* ----- chess ----- */
  .chess-card {
    /* Two lines of meta and the air above them. The column is the band's
       height less this, so the square board fills the column's width and board
       plus meta comes to exactly the band's height.

       The second term is the ceiling. On a tall window the leftover the band
       gets is large, and three boxes each as wide as the band is tall add up
       to more than the band is wide: at 2560x1300 they ran off the end of it.
       Past that point width is the bound, so the column takes the smaller of
       the two and the cards centre in the height they no longer need. */
    --stack: calc(2.7 * var(--fs-ui) + 12px);
    flex: 0 0 clamp(140px, calc(27vh / var(--zoom)), 300px);
    flex-basis: min(calc(100cqh - var(--stack)), 29cqw);
    justify-content: center;
  }
  .chess-board {
    flex: 0 0 auto;
    width: 100%;
    aspect-ratio: 1;
    display: grid;
    grid-template-columns: repeat(8, 1fr);
    grid-template-rows: repeat(8, 1fr);
    border: 1px solid var(--border);
    border-radius: 3px;
    overflow: hidden;
    background: #16352b;
    /* so the squares can size their glyphs against the board itself rather
       than the viewport - the board's height is set by the flex line above. */
    container-type: size;
  }
  .chess-board .sq {
    display: flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
    /* vh first for anything without container query units, cqmin after.
       A square is an eighth of the board, so the glyph is sized against that:
       8.6cqmin is ~69% of one square, not 69% of the whole board. */
    font-size: clamp(12px, calc(2.9vh / var(--zoom)), 33px);
    font-size: 8.6cqmin;
    background: #16352b;
  }
  /* a1 is a dark square and h1 a light one - the parity below puts .dark on a1,
     so .dark has to be the darker of the two or the board is inverted. */
  .chess-board .sq.dark { background: #0c1c16; }
  .chess-board .sq.w { color: #e8ece9; }
  .chess-board .sq.b { color: var(--accent); }
  .chess-meta {
    flex: 0 0 auto;
    height: var(--stack);
    padding-top: 12px;
    line-height: 1.35;
    font-size: var(--fs-ui);
  }
  .chess-stats { display: flex; gap: clamp(8px, calc(0.9vw / var(--zoom)), 18px); }
  .chess-stats .cs { color: var(--fg); }
  .chess-stats .cs-k {
    color: var(--accent);
    font-size: var(--fs-micro);
    text-transform: uppercase;
    letter-spacing: 1px;
    margin-right: 6px;
  }
  .chess-link {
    display: block;
    color: var(--muted);
    transition: color 0.15s;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .chess-card:hover .chess-link,
  .chess-card:focus-visible .chess-link { color: var(--accent); }
  .chess-card:hover .chess-board { border-color: var(--dim); }

  /* ----- photography ----- */
  /* A centred stack: the framed photo with an arrow either side, the place it
     was taken, and a dot per photo. The frame is 3:4 and derived from the
     band's height the same way the two squares beside it are, with the same
     cqw ceiling for a tall window. */
  .photo-card {
    flex: 1 1 auto;
    align-items: center;
    justify-content: center;
    gap: clamp(7px, calc(1vh / var(--zoom)), 14px);
    /* the place line, the dots, and the two gaps above them */
    --stack: calc(1.35 * var(--fs-ui) + 9px + 28px);
  }
  .photo-stage {
    display: flex;
    align-items: center;
    gap: clamp(8px, calc(1.2vw / var(--zoom)), 24px);
    max-width: 100%;
  }
  .photo-arrow {
    flex: 0 0 auto;
    font: inherit;
    font-size: var(--fs-hero);
    line-height: 1;
    color: var(--dim);
    background: none;
    border: 1px solid transparent;
    border-radius: 4px;
    padding: 6px 14px 12px;
    cursor: pointer;
    transition: color 0.15s, border-color 0.15s;
  }
  .photo-arrow:hover { color: var(--accent); border-color: var(--border); }
  .photo-arrow:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; }
  /* Matted rather than a hairline box: the padding is the mount, the inner
     hairline is the rebate, and the shadow lifts the whole thing off the
     terminal so it reads as a print on the wall instead of a div with a
     border. box-sizing is border-box globally, so the mount comes out of the
     3:4 the width above resolves, and the outer box stays exact. */
  .photo-frame {
    /* min-width: 0 so it can give way to the arrows beside it rather than
       pushing them off the screen - a flex item's automatic minimum is its
       content, and on a phone the frame plus two arrows is wider than 360px. */
    flex: 0 1 auto;
    min-width: 0;
    width: min(calc(0.75 * (100cqh - var(--stack))), 22cqw);
    aspect-ratio: 3 / 4;
    padding: clamp(4px, calc(0.65vh / var(--zoom)), 9px);
    background: linear-gradient(160deg, #16332a, #0d1f19);
    border: 1px solid var(--dim);
    border-radius: 4px;
    box-shadow: 0 10px 28px rgba(0, 0, 0, 0.5);
  }
  .photo-frame img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    border-radius: 1px;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55);
    opacity: 1;
    transition: opacity 0.28s ease;
  }
  .photo-frame img.swapping { opacity: 0; }
  .photo-place {
    flex: 0 0 auto;
    font-size: var(--fs-ui);
    line-height: 1.35;
    letter-spacing: 0.5px;
    color: var(--muted);
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
  }
  .photo-dots { flex: 0 0 auto; display: flex; gap: 9px; align-items: center; }
  .photo-dot {
    width: 7px;
    height: 7px;
    padding: 0;
    border-radius: 50%;
    border: 1px solid var(--dim);
    background: none;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
  }
  .photo-dot:hover { border-color: var(--accent); }
  .photo-dot:focus-visible { outline: 1px solid var(--accent); outline-offset: 3px; }
  .photo-dot[aria-current="true"] { background: var(--accent); border-color: var(--accent); }

  /* ----- on repeat ----- */
  .track-card {
    /* Label, title, artist, the bar and the times, plus the gaps between the
       six rows. Same trick as the chess column above. Split in two because the
       container query at the bottom of this block takes a row away. */
    --rows: calc(1.35 * var(--fs-ui) + 2.7 * var(--fs-micro));
    --gaps: 27px;
    flex: 0 0 clamp(120px, calc(21vh / var(--zoom)), 250px);
    flex-basis: min(calc(100cqh - var(--rows) - var(--gaps)), 28cqw);
    justify-content: center;
    gap: 6px;
  }
  .track-art {
    flex: 0 0 auto;
    width: 100%;
    aspect-ratio: 1;
    border: 1px solid var(--dim);
    border-radius: 4px;
    background: #0c1c16;
    overflow: hidden;
    box-shadow: 0 10px 28px rgba(0, 0, 0, 0.5);
  }
  .track-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
  /* The card turns over on a timer like the photo strip beside it, so the
     cover and the three lines under it cross-fade together. The bar is left
     out: it slides to the new position instead, which reads as the track
     changing rather than the card blinking. */
  .track-art img,
  .track-name,
  .track-artist,
  .track-times { transition: opacity 0.28s ease; }
  .track-bar span { transition: width 0.28s ease; }
  .track-card.swapping .track-art img,
  .track-card.swapping .track-name,
  .track-card.swapping .track-artist,
  .track-card.swapping .track-times { opacity: 0; }
  .track-name {
    font-size: var(--fs-ui);
    line-height: 1.35;
    color: var(--fg);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .track-artist {
    font-size: var(--fs-micro);
    line-height: 1.35;
    color: var(--muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* Ornamental. Nothing is actually playing, which is why the card says
     "on repeat" rather than "now playing"; the position comes from `at` in
     content.js. A real now-playing needs a token and a server to hold it, and
     this site is a folder on GitHub Pages. */
  .track-bar {
    height: 3px;
    border-radius: 2px;
    background: var(--border);
    overflow: hidden;
  }
  .track-bar span {
    display: block;
    height: 100%;
    width: 0;
    background: var(--accent);
  }
  .track-times {
    display: flex;
    justify-content: space-between;
    font-size: var(--fs-micro);
    line-height: 1.35;
    color: var(--dim);
  }
  .track-card:hover .track-art,
  .track-card:focus-visible .track-art { border-color: var(--accent); }

  /* The band is the window's leftover, so a shorter window squeezes all three
     cards at once - and the track card is the one that cannot take it. Its
     rows of text are a near-fixed height, so the cover is the only thing left
     to shrink, and on a 756px-tall window it came out a 68px thumbnail next to
     a 125px board. The times row is what the card can most afford to lose, so
     below the height where that stops being true it goes and the cover gets
     the row back. Queried against the band, which is already a size container
     for the columns above.

     255px, not the ~205 it looks like on screen: a container query is measured
     in the container's own pixels, and the whole terminal is zoomed to 0.8. */
  @container (max-height: 255px) {
    .track-times { display: none; }
    .track-card {
      --rows: calc(1.35 * var(--fs-ui) + 1.35 * var(--fs-micro));
      --gaps: 21px;
    }
  }

  /* The band is bound by what the window has left after the portrait column -
     measured, with no heading above it, that is band = height - 595. Below
     roughly 715px there is no longer room for a band anyone can read, so drop
     it and hand the space back to the grid, which is exactly the layout that
     shipped before it existed. Phones are excluded: they scroll, so the band
     is sized from the width there and always has room.
     The second query is the 821-980px band: the grid has collapsed to a single
     column there but the window is still pinned to the viewport, so the
     portrait and the content stack and leave nothing over - the band pushed
     home 53px past the screen. */
  @media (min-width: 821px) and (max-height: 714px),
         (min-width: 821px) and (max-width: 980px) {
    .home-band { display: none; }
    #home .main-grid { flex: 1; }
  }
  /* Sits between the scrolling .screen and the footer bar, so it never scrolls
     away. flex-shrink: 0 keeps the flex column from squeezing it. */
  .tabs {
    flex-shrink: 0;
    position: relative;
    z-index: 2;
    border-top: 1px dashed #1f2128;
    padding-left: clamp(20px, calc(1.8vw / var(--zoom)), 40px);
    padding-right: clamp(20px, calc(1.8vw / var(--zoom)), 40px);
    display: flex;
    gap: clamp(18px, calc(2vw / var(--zoom)), 34px);
    padding-top: clamp(6px, calc(1vh / var(--zoom)), 15px);
    margin-top: 0;
    padding-bottom: clamp(6px, calc(1vh / var(--zoom)), 15px);
    font-size: var(--fs-ui);
    flex-wrap: wrap;
  }
  .tab {
    cursor: pointer;
    color: var(--fg);
    padding-bottom: 2px;
    border-bottom: 1px solid transparent;
    transition: color 0.15s;
    background: none;
    border: none;
    font-family: inherit;
    font-size: var(--fs-ui);
    padding: 0 0 2px;
  }
  .tab:hover { color: var(--accent); }
  .tab.active { color: var(--accent); }
  .tab.active::before { content: '+ '; color: var(--accent); }
  .tab:not(.active)::before { content: '  '; white-space: pre; }

  /* Paired hints: .kb-hint carries the keyboard instructions, .touch-hint the
     phone wording. Exactly one of the two is visible at any width. */
  .touch-hint { display: none; }

  .section-footer {
    margin-top: auto;
    padding-top: clamp(5px, calc(1vh / var(--zoom)), 14px);
    font-size: var(--fs-micro);
    color: var(--dim);
    padding-left: 12px;
    border-left: 2px solid var(--border);
    max-width: 760px;
  }

  .footer-bar {
    padding: clamp(9px, calc(1.2vh / var(--zoom)), 16px) clamp(20px, calc(1.8vw / var(--zoom)), 40px);
    color: var(--muted);
    font-size: var(--fs-micro);
    border-top: 1px dashed #1f2128;
    display: flex;
    justify-content: space-between;
    align-items: center;
    position: relative;
    z-index: 2;
    gap: 10px;
    flex-shrink: 0;
  }
  .footer-bar .keys { color: var(--muted); }
  .footer-bar .keys kbd { color: var(--fg); font-family: inherit; font-size: inherit; padding: 0 2px; }
  .footer-bar .right { color: var(--dim); display: flex; gap: 12px; align-items: center; }
  .footer-bar .right .sep { color: var(--border); }
  .footer-bar .right .loc { color: var(--muted); }
  .footer-bar .right a { color: var(--muted); text-decoration: none; transition: color 0.15s; }
  .footer-bar .right a:hover { color: var(--accent); }

  .section {
    display: none;
    flex-direction: column;
    flex: 1;
  }
  .section.active { display: flex; animation: fade-in 0.18s ease-out; }
  @keyframes fade-in {
    from { opacity: 0; transform: translateY(2px); }
    to { opacity: 1; transform: translateY(0); }
  }

  .section-content {
    flex: 1;
    display: flex;
    flex-direction: column;
  }

  .section-head {
    color: var(--accent);
    font-size: var(--fs-head);
    font-weight: 600;
    margin: 0 0 1px;
    letter-spacing: 0.5px;
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    padding-bottom: 7px;
    border-bottom: 1px solid var(--border);
    text-transform: uppercase;
  }
  .section-head .count {
    color: var(--muted);
    font-size: var(--fs-micro);
  }

  .proj {
    padding: clamp(17px, calc(2.2vh / var(--zoom)), 26px) 18px;
    border: 1px solid transparent;
    border-radius: 3px;
    border-bottom: 1px dashed #1f2128;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
  }
  .proj:last-child { border-bottom: none; }
  .proj:hover, .proj.focused {
    background: rgba(0, 229, 160, 0.04);
    border-color: var(--border);
  }
  .proj.focused { border-color: var(--accent); }
  .proj-title { color: var(--accent); font-size: var(--fs-lead); margin-bottom: 4px; }
  .proj-title::after {
    content: ' ↗';
    color: var(--muted);
    font-size: 13px;
    opacity: 0;
    transition: opacity 0.15s;
  }
  .proj:hover .proj-title::after, .proj.focused .proj-title::after { opacity: 1; }
  .proj-meta { color: var(--muted); font-size: var(--fs-ui); margin-bottom: 8px; }
  .proj-desc { color: var(--fg); font-size: var(--fs-body); line-height: 1.6; }

  .chip {
    display: inline-block;
    padding: 1px 7px;
    margin: 5px 4px 0 0;
    font-size: var(--fs-micro);
    color: var(--accent);
    border: 1px solid var(--border);
    border-radius: 2px;
    background: rgba(0, 50, 32, 0.2);
  }

  .tl-entry {
    margin-bottom: clamp(3px, calc(0.6vh / var(--zoom)), 11px);
    padding-left: 13px;
    border-left: 2px solid var(--border);
  }
  .tl-date { color: var(--warn); font-size: var(--fs-micro); line-height: 1.25; }
  /* The degree dates are the one line in ~/education that reads as a heading,
     so they carry 2pt over the timeline dates elsewhere. */
  #education .tl-date { font-size: calc(var(--fs-micro) + 2.7px); }
  .tl-role { color: var(--accent); font-size: var(--fs-ui); margin-top: 1px; line-height: 1.2; }
  .tl-org { color: var(--fg); font-size: var(--fs-micro); line-height: 1.2; }
  .tl-desc { color: var(--muted); font-size: var(--fs-ui); margin-top: 3px; line-height: 1.32; max-width: none; }
  /* Marker matches the one the project titles already use, rather than a disc. */
  .tl-desc ul { margin: 2px 0 0; padding: 0; list-style: none; }
  .tl-desc li { position: relative; padding-left: 15px; margin-bottom: 1px; }
  .tl-desc li:last-child { margin-bottom: 0; }
  .tl-desc li::before {
    content: '▸';
    position: absolute;
    left: 0;
    color: var(--dim);
  }

  .main-grid > * { min-width: 0; }
  /* Was an inline style on the markup, which no media query could override. */
  .course-grid {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    gap: clamp(5px, calc(0.9vh / var(--zoom)), 12px) 40px;
    font-size: var(--fs-ui);
  }

  /* The org cards were butted straight up against their heading. */
  #education .section-head + .org-list,
  #education .org-list { margin-top: clamp(4px, calc(0.9vh / var(--zoom)), 13px); }
  #education .course-grid { margin-bottom: clamp(4px, calc(0.9vh / var(--zoom)), 12px); }

  /* Was .section-grid, a two-column layout with a sidebar. The sidebars are
     gone, so the content simply runs the full width. */
  .section-body { min-width: 0; }

  .org-list {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
    gap: clamp(10px, calc(1.4vh / var(--zoom)), 18px);
    max-width: none;
  }
  .org-card {
    padding: clamp(9px, calc(1.2vh / var(--zoom)), 15px) 15px;
    border: 1px solid var(--border);
    border-radius: 3px;
    background: rgba(255, 189, 46, 0.03);
  }
  .org-card .name {
    color: var(--warn);
    font-size: var(--fs-lead);
    font-weight: 500;
    margin-bottom: 2px;
  }
  .org-card .role {
    color: var(--muted);
    font-size: var(--fs-micro);
    text-transform: uppercase;
    letter-spacing: 1px;
    margin-bottom: 5px;
  }
  .org-card .desc {
    color: var(--fg);
    font-size: var(--fs-ui);
    line-height: 1.6;
  }

  .link {
    color: var(--accent);
    text-decoration: none;
    border-bottom: 1px dotted var(--border);
  }
  .link:hover { border-bottom-color: var(--accent); }


  .cursor {
    display: inline-block;
    width: 8px; height: 17px;
    background: var(--accent);
    vertical-align: middle;
    animation: blink 1.1s steps(2) infinite;
    margin-left: 2px;
  }

  /* ===== Blog & LinkedIn feed (two tabs side by side) ===== */
  .blog-tabs {
    display: flex;
    gap: 6px;
    margin-bottom: 18px;
    border-bottom: 1px solid var(--border);
  }
  .blog-tab {
    cursor: pointer;
    background: transparent;
    border: 1px solid transparent;
    border-bottom: none;
    padding: 10px 20px;
    color: var(--muted);
    font-family: inherit;
    font-size: 15px;
    border-radius: 3px 3px 0 0;
    transition: color 0.15s, background 0.15s, border-color 0.15s;
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .blog-tab:hover { color: var(--fg); }
  .blog-tab.active {
    color: var(--accent);
    background: rgba(0, 229, 160, 0.04);
    border-color: var(--border);
    border-bottom-color: var(--bg);
    margin-bottom: -1px;
  }
  .blog-tab .badge {
    font-size: 12px;
    color: var(--muted);
    background: rgba(122, 138, 130, 0.15);
    padding: 1px 6px;
    border-radius: 8px;
  }
  .blog-tab.active .badge {
    color: var(--accent);
    background: rgba(0, 229, 160, 0.12);
  }

  .blog-post {
    padding: 20px 22px;
    margin-bottom: 14px;
    border: 1px solid var(--border);
    border-radius: 3px;
    background: rgba(0, 229, 160, 0.02);
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
  }
  .blog-post:hover, .blog-post.focused {
    background: rgba(0, 229, 160, 0.05);
    border-color: var(--accent);
  }
  .blog-post .date { color: var(--warn); font-size: 14px; }
  .blog-post .title { color: var(--accent); font-size: var(--fs-lead); margin: 4px 0 6px; }
  .blog-post .excerpt { color: var(--fg); font-size: var(--fs-ui); line-height: 1.6; max-width: none; }
  .blog-post .read-more { color: var(--muted); font-size: 14px; margin-top: 10px; }

  .li-post {
    padding: 20px 22px;
    margin-bottom: 14px;
    border: 1px solid var(--border);
    border-radius: 3px;
    background: rgba(10, 102, 194, 0.04);
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 14px;
    align-items: start;
  }
  .li-post:hover { background: rgba(10, 102, 194, 0.07); }
  .li-post .li-icon {
    width: 28px; height: 28px;
    border-radius: 3px;
    background: var(--linkedin);
    color: #fff;
    display: flex; align-items: center; justify-content: center;
    font-size: 15px; font-weight: 600;
    font-family: inherit;
    flex-shrink: 0;
  }
  .li-post .li-meta { color: var(--muted); font-size: 13px; margin-bottom: 4px; }
  .li-post .li-meta .li-date { color: var(--warn); }
  .li-post .li-meta .li-sep { color: var(--dim); margin: 0 6px; }
  .li-post .li-body { color: var(--fg); font-size: var(--fs-ui); line-height: 1.6; max-width: none; white-space: pre-wrap; }
  .li-post .li-link {
    display: inline-block;
    margin-top: 10px;
    color: var(--linkedin);
    font-size: 14px;
    text-decoration: none;
    border-bottom: 1px dotted rgba(10, 102, 194, 0.4);
  }
  .li-post .li-link:hover { border-bottom-color: var(--linkedin); }

  /* The RSS/LinkedIn line and the section footer were two stacked rows saying
     much the same thing. One row instead, links pushed right, so ~/blog ends
     flush at the bottom on a single line like every other section. */
  .feed-footer {
    /* .section-footer caps at 760px, which wrapped the links onto their own
       line and put the stack straight back. */
    max-width: none;
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: clamp(10px, calc(2vw / var(--zoom)), 30px);
    flex-wrap: wrap;
  }
  .feed-link { color: var(--muted); }
  .feed-link a {
    color: var(--accent);
    text-decoration: none;
    border-bottom: 1px dotted var(--border);
  }
  .feed-link a:hover { border-bottom-color: var(--accent); }


  /* ===== CONTACT =====
     Everything used to butt together. The blurb, the form and the sign-off are
     three separate beats and are spaced as such. */
  /* The form sat at its natural height and left a dead band above // offline.
     The chain from the section down to the textarea is flex now, so the
     message box takes whatever vertical space is going. */
  #contact .section-body { flex: 1; display: flex; flex-direction: column; min-height: 0; }
  .contact-main { min-width: 0; flex: 1; display: flex; flex-direction: column; min-height: 0; }
  .contact-blurb {
    color: var(--fg);
    font-size: var(--fs-ui);
    line-height: 1.5;
    margin-bottom: clamp(14px, calc(2.2vh / var(--zoom)), 26px);
  }

  /* The auto margin sits here now that // offline is gone - it is what carries
     the sign-off down to the foot of the section instead of leaving it stranded
     under the form. The footer below drops its own auto or the two would split
     the gap between them. */
  .contact-exit {
    margin-top: auto;
    padding-top: clamp(14px, calc(2vh / var(--zoom)), 24px);
    color: var(--muted);
    font-size: var(--fs-ui);
    line-height: 1.45;
  }
  #contact .section-footer { margin-top: clamp(8px, calc(1.4vh / var(--zoom)), 18px); }

  .form-head {
    color: var(--accent);
    font-size: var(--fs-ui);
    margin-bottom: clamp(5px, calc(0.9vh / var(--zoom)), 12px);
    text-transform: uppercase;
    letter-spacing: 1px;
  }

  .contact-form {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
    padding: clamp(8px, calc(1.2vh / var(--zoom)), 16px) clamp(20px, calc(2.2vw / var(--zoom)), 34px);
    border: 1px solid var(--border);
    border-radius: 3px;
    background: rgba(0, 229, 160, 0.02);
    width: 100%;
  }
  .message-line { flex: 1; min-height: 0; flex-direction: column; align-items: stretch; }
  .form-line { display: flex; align-items: baseline; gap: 14px; margin-bottom: clamp(6px, calc(1vh / var(--zoom)), 13px); font-size: var(--fs-ui); }
  .form-line label { color: var(--accent); min-width: 90px; flex-shrink: 0; }
  .form-line .prompt-char { color: var(--warn); }
  .form-input, .form-textarea {
    flex: 1;
    background: transparent;
    border: none;
    outline: none;
    color: var(--fg);
    font-family: inherit;
    font-size: var(--fs-body);
    padding: 6px 4px;
    border-bottom: 1px dashed var(--border);
    caret-color: var(--accent);
  }
  .form-textarea {
    resize: vertical;
    /* Roughly ten lines. It used to be four, which meant the box scrolled on
       its own tiny scrollbar before the message was even finished. */
    flex: 1;
    min-height: clamp(110px, calc(14.5vh / var(--zoom)), 190px);
    font-size: var(--fs-body);
    font-size: 15px;
    border: 1px dashed var(--border);
    border-radius: 2px;
    padding: 10px 12px;
    line-height: 1.6;
    width: 100%;
  }
  .form-input:focus, .form-textarea:focus { border-color: var(--accent); }
  .form-submit {
    /* The form is a flex column now so the message box can grow; without this
       the button stretches the full width of it. */
    align-self: flex-start;
    margin-top: clamp(5px, calc(0.9vh / var(--zoom)), 12px);
    background: transparent;
    color: var(--accent);
    border: 1px solid var(--border);
    padding: 0.5em 1.6em;
    font-family: inherit;
    font-size: var(--fs-ui);
    cursor: pointer;
    border-radius: 2px;
    transition: background 0.15s, border-color 0.15s;
  }
  .form-submit:hover { background: rgba(0, 229, 160, 0.1); border-color: var(--accent); }
  .form-submit:disabled { color: var(--muted); cursor: not-allowed; }
  .form-status { margin-top: 5px; font-size: var(--fs-micro); color: var(--muted); min-height: 0; }
  .form-status.ok { color: var(--accent); }
  .form-status.err { color: var(--danger); }

  .modal-overlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.75);
    z-index: 100;
    align-items: center;
    justify-content: center;
    padding: clamp(10px, 1.8vh, 20px);
    animation: fade-in 0.15s ease-out;
  }
  .modal-overlay.active { display: flex; }
  .modal {
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 6px;
    max-width: 1180px;
    width: 100%;
    /* Divided back out for the same reason .terminal divides its height: the
       box sizes itself in its own zoomed units, so a plain 96vh would only
       reach 77% of the window and push the stack and chips below a scrollbar.
       The width is deliberately not compensated - a narrower modal is part of
       reading as 80%. */
    max-height: calc(96vh / var(--zoom));
    overflow: hidden;
    display: flex;
    flex-direction: column;
    font-family: inherit;
    box-shadow: 0 10px 50px rgba(0, 229, 160, 0.08);
  }
  .modal-header {
    padding: 11px 16px;
    background: var(--header);
    border-bottom: 1px solid #23232e;
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: var(--fs-ui);
  }
  .modal-header .title { flex: 1; color: #8a8a9a; text-align: center; }
  .modal-header .close {
    cursor: pointer;
    color: var(--muted);
    padding: 2px 8px;
    border-radius: 2px;
    background: none;
    border: none;
    font-family: inherit;
    font-size: var(--fs-ui);
  }
  .modal-header .close:hover { color: var(--danger); }
  .modal:focus { outline: none; }
  .modal:focus-visible { outline: none; }
  .modal-body { padding: 30px 36px; overflow-y: auto; flex: 1; }
  .modal-title { color: var(--accent); font-size: var(--fs-head); margin: 0 0 6px; }
  .modal-meta { color: var(--muted); font-size: var(--fs-ui); margin-bottom: 18px; }
  .modal-desc { color: var(--fg); font-size: var(--fs-body); line-height: 1.8; margin-bottom: 18px; }
  .modal-desc p { margin: 0 0 14px; }
  .modal-desc h3 {
    color: var(--warn);
    font-size: var(--fs-body);
    margin: 18px 0 10px;
    font-weight: 500;
  }
  .modal-desc code {
    background: rgba(0, 229, 160, 0.08);
    color: var(--accent);
    padding: 1px 6px;
    border-radius: 2px;
    font-size: var(--fs-ui);
  }
  .modal-media {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
    margin-bottom: 18px;
  }
  .modal-media .media-slot {
    background: rgba(0, 229, 160, 0.04);
    border: 1px dashed var(--border);
    border-radius: 3px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--dim);
    font-size: var(--fs-micro);
    text-align: center;
    padding: 12px;
    position: relative;
    overflow: hidden;
  }
  .modal-media .media-slot.has-media { border-style: solid; padding: 0; overflow: visible; }
  .modal-media .media-slot.full-width {
    grid-column: 1 / -1;
    aspect-ratio: 16 / 9;
  }
  .modal-media .media-slot.logo-slot {
    aspect-ratio: auto;
    padding: 34px 24px;
    background: rgba(0, 229, 160, 0.02);
  }
  .modal-media .media-slot.logo-slot img { max-width: 300px; margin: 0 auto; }
  .modal-media img {
    width: 100%; height: auto;
    object-fit: contain;
    border: none;
  }
  .modal-media video {
    width: 100%; height: auto;
    border: none;
  }
  .modal-media iframe {
    width: 100%; height: 100%;
    object-fit: cover;
    border: none;
  }
  .modal-media .placeholder-icon {
    font-family: inherit;
    color: var(--accent);
    font-size: 26px;
    margin-bottom: 4px;
    opacity: 0.4;
  }
  /* ===== media slideshow ===== */
  .modal-carousel { margin-bottom: 18px; }
  /* A fixed-height stage, because the slides have wildly different shapes
     (0.75 to 1.83) and the images carry no width/height. Sizing to the media
     made the modal jump on every load and again on every slide change. */
  .carousel-viewport {
    position: relative;
    height: min(62vh, 620px);
    background: rgba(0, 229, 160, 0.03);
    border: 1px solid var(--border);
    border-radius: 3px;
    overflow: hidden;
  }
  .carousel-slide {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .carousel-slide[hidden] { display: none; }
  /* A definite height, not just a max: the slot is not inside .modal-media here
     so it never picked up that rule's display, and a percentage max-height
     against an auto-height parent resolves to none. The portrait HFA clip came
     out at its natural height and was clipped top and bottom by the viewport. */
  .carousel-slide .media-slot {
    border: none;
    border-radius: 0;
    background: transparent;
    padding: 0;
    aspect-ratio: auto;
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  /* width/height auto keeps a small source at its native size rather than
     scaling it up to fill the box. */
  .carousel-slide img,
  .carousel-slide video {
    display: block;
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
  }
  .carousel-slide .media-slot.logo-slot img { max-width: 300px; }
  .carousel-nav {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 14px;
    margin-top: 8px;
  }
  .carousel-btn {
    font-family: inherit;
    font-size: var(--fs-ui);
    line-height: 1;
    color: var(--muted);
    background: transparent;
    border: 1px solid var(--border);
    border-radius: 2px;
    padding: 4px 13px;
    cursor: pointer;
    transition: color 0.15s, border-color 0.15s;
  }
  .carousel-btn:hover { color: var(--accent); border-color: var(--accent); }
  .carousel-count {
    font-size: var(--fs-micro);
    color: var(--muted);
    min-width: 44px;
    text-align: center;
  }


  /* ===== interactive board view (assets/js/board3d.js) ===== */
  .modal-media .media-slot.board-slot {
    /* Squarer than the 16/9 the other full-width slots use — the board is round
       and the tall parts need headroom. */
    aspect-ratio: auto;
    display: block;
    padding: 0;
    background: radial-gradient(ellipse at 50% 38%, #12211b 0%, #080b0a 70%);
    border-style: solid;
  }
  .board3d-stage { position: relative; height: min(62vh, 620px); }
  /* In a carousel the board is a full-bleed stage, not a picture sitting at its
     own size the way the photo slides do. */
  /* Column, not a 100%-height stage: the schematic/flip bar is a sibling below
     the stage, so a stage that took the full height pushed the bar past the
     viewport's overflow: hidden and the buttons could be neither seen nor
     clicked. The stage takes what is left instead. */
  .carousel-slide .media-slot.board-slot {
    flex-direction: column;
    align-items: stretch;   /* the stage and its bar span the width */
  }
  .carousel-slide .board3d-stage {
    flex: 1;
    height: auto;
    min-height: 0;        /* or the flex child refuses to shrink */
  }
  .board3d-canvas {
    display: block;
    width: 100%;
    height: 100%;
    cursor: grab;
    /* Without this the browser claims the drag for scrolling and the board
       never rotates on a touchscreen. */
    touch-action: none;
  }
  .board3d-canvas:active { cursor: grabbing; }
  .board3d-canvas:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; }
  .board3d-status {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--muted);
    font-size: var(--fs-micro);
    pointer-events: none;
  }
  /* Schematic overlay: same stage, swapped in over the canvas. */
  .board3d-schematic {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 8px;
    text-decoration: none;
  }
  .board3d-schematic[hidden] { display: none; }
  .board3d-schematic img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    border-radius: 2px;
  }
  .board3d-openfull {
    position: absolute;
    right: 10px;
    bottom: 8px;
    font-size: var(--fs-micro);
    color: var(--muted);
    background: rgba(8, 11, 10, 0.82);
    border: 1px solid var(--border);
    border-radius: 2px;
    padding: 2px 7px;
  }
  .board3d-schematic:hover .board3d-openfull { color: var(--accent); border-color: var(--accent); }
  .board3d-canvas[hidden] { display: none; }

  .board3d-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
    padding: 8px 12px;
    border-top: 1px solid var(--border);
    font-size: var(--fs-micro);
    color: var(--muted);
  }
  .board3d-actions { display: flex; gap: 6px; }
  .board3d-bar button {
    font-family: inherit;
    font-size: var(--fs-micro);
    color: var(--muted);
    background: transparent;
    border: 1px solid var(--border);
    border-radius: 2px;
    padding: 4px 10px;
    cursor: pointer;
    transition: color 0.15s, border-color 0.15s;
  }
  .board3d-bar button:hover:not(:disabled) { color: var(--accent); border-color: var(--accent); }
  .board3d-bar button:disabled { opacity: 0.35; cursor: default; }

  .modal-section-head {
    color: var(--warn);
    font-size: var(--fs-ui);
    margin: 16px 0 8px;
    text-transform: uppercase;
    letter-spacing: 1px;
  }

  /* ===== Responsive =====
     Desktop is a fixed terminal window; everything below reflows into one
     column. This used to stop at 768px, where the whole terminal was replaced
     by a "best viewed on desktop" notice — but the site's URL goes on job
     applications and LinkedIn, so most visitors arrive on a phone and that
     notice was the entire site for them. */

  /* Two-column grids collapse once the sidebar can no longer hold its minimum. */
  @media (max-width: 980px) {
    .main-grid { grid-template-columns: 1fr; gap: 24px; }
    .portrait-col { flex-direction: row; flex-wrap: wrap; align-items: flex-start; }
    .photo-portrait { max-width: 320px; }
  }

  /* Tablet and below: the layout is a single column and there is no keyboard. */
  @media (max-width: 820px) {
    .screen { padding: 18px 20px 20px; }
    .boot-screen { padding: 20px; font-size: 14px; }
    .sysinfo { font-size: 13px; }
    .modal-media { grid-template-columns: 1fr; }
    .form-line { flex-direction: column; gap: 4px; align-items: stretch; }
    .form-line label { min-width: 0; }
    .terminal { min-height: auto; }
    .li-post { grid-template-columns: 1fr; }
    .brand { font-size: 11px; }

    /* Keyboard hints are dead weight without a keyboard: the footer legend,
       and the ones written into the section headers and footers. */
    .footer-bar .keys { display: none; }
    .footer-bar { justify-content: center; }
    .kb-hint { display: none; }
    .touch-hint { display: inline; }

    /* Tabs are now the only way through the site, so they get real tap targets.
       They wrap onto a second row rather than scrolling sideways — all seven
       sections stay visible, which matters more here than staying on one line. */
    .tabs { gap: 10px 20px; padding-top: 18px; }
    .tab { padding: 6px 0; }

    /* The page scrolls here, so nothing bounds the band's height and every
       card has to go back to sizing from its own width. That also means
       dropping the size containment: a contained box takes no height from its
       contents, which is right when the band is the leftover of a fixed window
       and collapses it to nothing when it is not. */
    /* Same reason as the band below: the strip is content-sized here, not the
       leftover of a fixed column, so it goes back to growing with its contents
       and they go back to the fluid scale. Left contained, the description
       wraps to three lines at this width and spills straight out of the
       bottom of a box that can no longer grow to hold it. */
    .now-building {
      container-type: normal;
      min-height: 0;
      flex: 0 0 auto;
    }
    .now-building .mark img { height: clamp(34px, 9vw, 48px); }
    .now-building .nb-label { font-size: var(--fs-micro); }
    .now-building .nb-name { font-size: var(--fs-lead); }
    .now-building .nb-desc { font-size: var(--fs-body); }

    .home-band {
      container-type: normal;
      flex: 0 0 auto;
      flex-direction: column;
      gap: 26px;
      margin-top: 22px;
      padding-inline: 0;
    }
    /* The rows stay stretched so the head and the times still sit at opposite
       ends of the card; it is the square that is pulled back to the left, by
       align-self below. */
    .chess-card, .track-card {
      flex: 0 0 auto;
      flex-basis: auto;
      padding-top: 6px;
    }
    .chess-board, .track-art, .photo-frame {
      flex: 0 0 auto;
      width: min(300px, 100%);
      align-self: flex-start;
    }
    .chess-meta { height: auto; }
    /* Left-aligned with everything else in the stacked band, and the frame
       sized from the width like the two squares above it. */
    .photo-card { align-items: flex-start; gap: 12px; }
    /* The arrows move onto the picture here rather than sitting either side of
       it. There is no spare width on a phone: beside the frame they pushed it
       past the screen, and shrinking the frame to fit them left the photo
       noticeably smaller than the board and the cover above it. */
    .photo-stage { position: relative; width: 100%; }
    .photo-arrow {
      position: absolute;
      top: 50%;
      transform: translateY(-50%);
      z-index: 1;
      color: var(--fg);
      background: rgba(10, 10, 15, 0.6);
      border-color: var(--border);
    }
    .photo-stage > .photo-arrow:first-child { left: 10px; }
    .photo-stage > .photo-arrow:last-child { right: 10px; }
    .photo-frame { width: min(300px, 100%); }
  }

  /* Phone. */
  @media (max-width: 640px) {
    body { padding: 10px 8px; }
    .terminal { border-radius: 6px; }
    .screen { padding: 16px 14px 18px; }
    .title-text {
      font-size: 13px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .motd { white-space: normal; overflow: visible; }
    .boot-screen { font-size: 13px; overflow-wrap: break-word; }
    .motd { font-size: 14px; }

    /* The ASCII wordmark cannot fit, so the real <h1> stops being invisible
       and takes over as the visible heading. */
    .brand { display: none; }
    .brand-text {
      position: static;
      width: auto;
      height: auto;
      overflow: visible;
      white-space: normal;
      clip-path: none;
      margin: 0 0 14px;
      font-size: 30px;
      font-weight: 500;
      line-height: 1.15;
      letter-spacing: 0.5px;
      color: var(--accent);
    }

    /* Name and pitch before the portrait: someone opening this from a job
       application wants to know who it is and find the resume button, and the
       photo plus sysinfo block reads fine as a footer. Neither column holds
       anything focusable, so tab order is unaffected by the swap. */
    .content-col { order: 1; }
    .portrait-col { order: 2; flex-direction: column; }
    .photo-portrait { max-width: 100%; }

    .section-head { font-size: 16px; }
    .course-grid { grid-template-columns: 1fr; }
    .quick-stats { grid-template-columns: 1fr 1fr; gap: 10px; }
    .stat-card { padding: 16px 18px; }
    .stat-card .num { font-size: 26px; }

    .now-building { flex-wrap: wrap; gap: 12px 14px; padding: 14px; }
    .now-building .mark { border-right: none; padding-right: 0; }
    .now-building .nb-name .tagline { margin-left: 0; display: block; }

    .proj { padding: 12px 14px; }
    .blog-post { padding: 16px; }
    .li-post { padding: 16px; }
    .blog-tab { padding: 10px 14px; }
    .contact-form { padding: 18px 16px; }
    .form-textarea { min-height: 140px; }

    /* Long words and URLs in prose are the usual source of sideways scroll. */
    .modal-desc, .li-post .li-body, .blog-post .excerpt, .tl-desc, .intro {
      overflow-wrap: break-word;
    }

    /* A centred dialog with a margin wastes a phone screen; go full-bleed. */
    .modal-overlay { padding: 0; }
    .modal {
      max-height: 100vh;
      max-height: 100dvh;
      height: 100%;
      border: none;
      border-radius: 0;
    }
    .modal-body { padding: 20px 16px; }
    .board3d-stage { height: 260px; }
    .carousel-viewport { height: 240px; }
    .carousel-slide img, .carousel-slide video { max-height: 240px; }
    .modal-header .close { padding: 8px 10px; }

    .footer-bar {
      padding: 12px 14px;
      font-size: 14px;
      flex-wrap: wrap;
      justify-content: center;
    }
    .footer-bar .right { flex-wrap: wrap; justify-content: center; row-gap: 4px; }
  }

  /* On anything desktop-sized the terminal is pinned to the viewport and the
     page itself never scrolls — a window that scrolls off the top of the screen
     is not a terminal. Long sections scroll inside .screen instead, on a thin
     scrollbar rather than the browser's full-height one. Phones keep ordinary
     page scrolling, where a fixed-height window would just trap the content. */
  @media (min-width: 821px) {
    :root { --zoom: 0.8; }
    html, body { height: 100%; overflow: hidden; }
    .terminal {
      /* zoom rather than a font-size cut: it scales padding, borders and the
         portrait in step with the type, which is what makes 80% look right
         instead of just small. The height is divided back out because the
         element sizes itself in its own zoomed units - at 0.8 a plain
         100dvh would render at 80% of the window and leave a band below. */
      zoom: var(--zoom);
      height: calc((100vh - 40px) / var(--zoom));
      height: calc((100dvh - 40px) / var(--zoom));
      min-height: 0;
    }
    .screen {
      overflow-y: auto;
      overscroll-behavior: contain;
      min-height: 0;          /* or the flex child refuses to shrink and scroll */
      scrollbar-width: thin;
      scrollbar-color: var(--border) transparent;
    }
    .screen::-webkit-scrollbar { width: 6px; }
    .screen::-webkit-scrollbar-track { background: transparent; }
    .screen::-webkit-scrollbar-thumb {
      background: var(--border);
      border-radius: 3px;
    }
    .screen::-webkit-scrollbar-thumb:hover { background: var(--dim); }

    /* The overlay is a sibling of .terminal, not a child - it has to be, so it
       can cover the window without inheriting the terminal's overflow: hidden.
       That also means it misses the zoom above, so it kept rendering at 100%
       against a page at 80% and every modal came out a quarter too big. Zoom it
       on its own. Nothing inside it divides --zoom back out: unlike .terminal,
       which has to fill the window, the modal is free to just be 20% smaller. */
    .modal-overlay { zoom: var(--zoom); }
  }

  /* Touch devices never fire :hover, so affordances that only appear on hover
     never appear at all. Show them outright instead. */
  @media (hover: none) {
    .proj-title::after { opacity: 1; }
  }

  @media (prefers-reduced-motion: reduce) {
    .cursor { animation: none; }
    .section.active { animation: none; }
  }
