/* menu.css — boot screen, header, channel grid, dock. */

*, *::before, *::after { box-sizing: border-box; }

/* Form controls do NOT inherit `font` from the document — a <button> is handed
   the UA's own family and size, so every button on this page was still Arial at
   13.333px after the whole site moved to the pixel face. The dock labels were
   the visible half of that; the invisible half is that 13.333px is fractional,
   and a bitmap face smears at fractional sizes.
   One reset instead of `font: inherit` sprinkled across seven rules, three of
   which already had it and four of which had been missed. */
button, input, select, textarea { font: inherit; }

/* min-height, not height: on a phone the content can exceed the viewport, and a
   hard 100% clipped it under the header. Desktop is pinned to 100dvh below. */
html { height: 100%; }

body {
  min-height: 100dvh;
  margin: 0;
  /* One face for the whole site — the same one the name chip has always used.
     resume.html deliberately keeps --font-ui; see the note in tokens.css. */
  font-family: var(--font-pix);
  font-size: var(--fs-body);
  /* Monospace runs wider per character and its glyphs are shorter, so the
     rounded face's 1.55 leaves paragraphs looking airy and hard to track. */
  line-height: 1.7;
  color: var(--ink);
  background: var(--ground);
  /* Graph paper. Layer order is top-down: the nodes sit above the heavy rules,
     which sit above the fine ones, over a soft vertical lift. Every layer is a
     repeating gradient rather than an image, so it costs nothing to download
     and retunes from four tokens. */
  background-image:
    radial-gradient(var(--grid-node) 1.1px, transparent 1.2px),
    repeating-linear-gradient(90deg, var(--grid-heavy) 0 1px, transparent 1px calc(var(--grid-step) * 5)),
    repeating-linear-gradient(180deg, var(--grid-heavy) 0 1px, transparent 1px calc(var(--grid-step) * 5)),
    repeating-linear-gradient(90deg, var(--grid-fine) 0 1px, transparent 1px var(--grid-step)),
    repeating-linear-gradient(180deg, var(--grid-fine) 0 1px, transparent 1px var(--grid-step)),
    linear-gradient(180deg, var(--ground-lift) 0%, var(--ground) 58%, var(--ground-edge) 100%);
  background-size:
    calc(var(--grid-step) * 5) calc(var(--grid-step) * 5),
    auto, auto, auto, auto, auto;
  background-position: calc(var(--grid-step) * 5 - .5px) calc(var(--grid-step) * 5 - .5px), 0 0, 0 0, 0 0, 0 0, 0 0;
  background-attachment: fixed;
  display: flex;
  flex-direction: column;
  overflow-x: hidden;
}

/* ── the pixel field ──────────────────────────────────────────────────────
 * A fixed canvas between the graph paper and everything else. It is BELOW the
 * hud (3), the stage (2) and the dock (3) and above nothing, so no text ever
 * sits on a moving pixel — the stars pass behind the cards, not through them.
 * pointer-events:none because a full-viewport canvas over the page would
 * otherwise swallow every click on the site. */
.stars {
  position: fixed; inset: 0; z-index: 1;
  width: 100%; height: 100%;
  pointer-events: none;
  /* the backing store is deliberately CSS-sized (see stars.js) — this keeps the
     browser's own upscale nearest-neighbour so the pixels stay square */
  image-rendering: pixelated;
}

.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

/* Icons are stroked, never filled. One rule keeps 40 glyphs consistent. */
.ico {
  width: 24px; height: 24px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.ico--sm { width: 15px; height: 15px; stroke-width: 2; }

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

.skip {
  position: absolute; left: 12px; top: 12px; z-index: 1200;
  background: var(--ink); color: var(--tile);
  padding: 10px 16px; border-radius: var(--r-sm);
  font-weight: 500; text-decoration: none;
  /* transform, not top — animating `top` re-solves the box tree every frame.
     It is one small element fired once on Tab, but it is the same mistake and
     the gate does not know the difference. */
  transform: translateY(-200%);
  transition: transform var(--d-fast) var(--ease);
}
.skip:focus { transform: none; }

/* A ring that has to stay visible on any ground the site has: white cards, the
   dark ground, the black boot screen, and the pale active row. */
:focus-visible {
  outline: 3px solid var(--focus-core);
  outline-offset: 2px;
  box-shadow: 0 0 0 5px var(--focus-halo);
  border-radius: var(--r-sm);
}

a { color: inherit; }

/* ── header ──────────────────────────────────────────────────────────── */
.hud {
  position: relative; z-index: 3;
  flex: none;
  height: var(--hud-h);
  display: flex; align-items: center; gap: 16px;
  padding: 10px clamp(12px, 2.5vw, 28px);
}

/* ── the name chip ────────────────────────────────────────────────────────
 * A name and a small glyph in a flat box. It has been, in order: a soft pastel
 * pill, then a portrait tile, then a miniature channel card — each one more
 * elaborate than the last for something whose entire job is to say whose site
 * this is and open About.
 *
 * So: no portrait, no role line, no sheen. The 2px rule and the radius are the
 * only things tying it to the wall, which is enough. */
.user {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 7px 12px;
  color: var(--card-projects-ink);
  background: var(--card-projects);
  border: 2px solid var(--card-rule); border-radius: var(--r-sm);
  cursor: pointer; font: inherit; text-align: left;
  position: relative;
  transition: transform var(--d-fast) var(--ease);
}
.user__glyph { width: 17px; height: 17px; flex: none; stroke-width: 2; opacity: .85; }
.user::after {
  content: '';
  position: absolute; inset: -2px;
  border-radius: inherit;
  box-shadow: 0 0 0 2px var(--ground), 0 0 0 4px var(--card-rule);
  opacity: 0; pointer-events: none;
  transition: opacity var(--d-fast) var(--ease);
}
.user:hover, .user:focus-visible { transform: translateY(-2px); }
.user:hover::after, .user:focus-visible::after { opacity: 1; }
.user:focus-visible { outline: 3px solid var(--card-projects-ink); outline-offset: -6px; }

/* The seven .av-* rules and the eight-shape portrait they styled are deleted
   along with the markup. Leaving a stylesheet full of selectors for elements
   nothing emits is worse than losing the drawing — it reads as live code and
   the next person to touch this file has to prove otherwise. */

.user__name {
  font-family: var(--font-pix);
  font-size: 14px; letter-spacing: .06em;
  color: inherit; white-space: nowrap;
}

.hud__right { display: flex; align-items: center; gap: 12px; }
.cornerbtn {
  width: 34px; height: 34px;
  display: grid; place-items: center;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  color: var(--ink-3); cursor: pointer;
  transition: color var(--d-fast) var(--ease), background var(--d-fast) var(--ease);
}
.cornerbtn:hover { color: var(--ink); background: var(--tile); border-color: var(--tile-edge); }
.cornerbtn .ico { width: 20px; height: 20px; }

.clock {
  font-family: var(--font-pix);
  font-size: var(--fs-small);
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
  letter-spacing: .06em;
}

/* ── stage ────────────────────────────────────────────────────────────────
 * A COLUMN, and the title is a real item in it. It used to be
 * `position: absolute; top: 0`, which took no space in the flex layout, carried
 * no z-index and sat before .grid in the DOM — so the positioned tiles painted
 * straight over the word PROJECTS at four of the eight viewports. Absolute
 * positioning to "get something out of the way" is the whole bug: it does not
 * get out of the way, it just stops being told where the way is. */
.stage {
  position: relative; z-index: 2;
  flex: 1 1 auto;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: clamp(10px, 1.8vh, 18px);
  padding: var(--stage-pad-y) var(--stage-pad);
  min-height: 0;
}

/* ── the pitch, and the level title ───────────────────────────────────────
 * One slot, two jobs. At the root it says what this is and why you would
 * bother; inside a channel it says which channel. Same element either way, so
 * the wall never shifts by a line when you open one. */
.level {
  flex: none;
  margin: 0; max-width: 62rem;
  text-align: center; text-wrap: balance;
}
.level[hidden] { display: none; }

.level__lead {
  display: block;
  font-family: var(--font-pix);
  font-size: 14px; letter-spacing: .1em;   /* stepped below; see the reflow block */
  text-transform: uppercase; color: var(--ink);
}
.level__sub {
  display: block; margin-top: 5px;
  font-size: var(--fs-small); line-height: 1.5; color: var(--ink-2);
}
/* Inside a channel there is only the one line, and it is quieter — the wall is
   the content there, not the sentence above it. */
body.is-sub .level__lead { font-size: 13px; letter-spacing: .18em; color: var(--ink-2); }


/* The Menu button is PERMANENT. It used to be display:none at the root, which
   re-centred the entire dock the moment you opened a channel — every other
   button jumped sideways. It is disabled at the root instead: the geometry
   stops moving and the way back is visible before you need it. */
.dock__btn:disabled { opacity: .3; cursor: default; }
.dock__btn:disabled:hover { transform: none; color: var(--ink-2); }
.dock__btn:disabled:hover::before { background: var(--tile); }

/* ── channel wall ─────────────────────────────────────────────────────────
 * One row, one tile per channel. --cols is set by grid.js from the data, so a
 * fourth channel needs no CSS.
 *
 * Tiles are CAPPED at the size the coverflow cards were — 330px wide, 4:3 —
 * rather than stretched to fill the row. Letting them take the whole width made
 * them 447px squares, which is a lot of black for three words. The cap goes on
 * --avail-w so every derived value downstream inherits it, instead of clamping
 * the grid afterwards and leaving the height maths reasoning about a width the
 * grid no longer has.
 *
 * Row height comes from the REAL stage box (height:100% + 1fr rows), never from
 * token arithmetic, because above 901px the page is overflow:hidden — a grid
 * that is too tall does not scroll, it clips silently. The two caps below are
 * duals: --w-cap comes from the height budget, --h-cap from the width budget,
 * exactly one binds at a time, and both produce a 4:3 tile. That is also why no
 * aspect-ratio is set here — a second definition of the same thing is free to
 * disagree with the first. */
.grid {
  --cols: 3;        /* overwritten by grid.js to match the channel count */
  --rows: 1;
  --gap: clamp(12px, 2.4vh, 22px);

  /* Wider than the old 330px, because the card is wider than it is tall now.
     460 x 192 against the old 330 x 247 is very nearly the same area — a card
     that grew in the dimension it needed and shrank in the one it did not. */
  --tile-max: 460px;
  --max-w: calc(var(--cols) * var(--tile-max) + (var(--cols) - 1) * var(--gap));

  /* the stage's inner width, capped, and the tile width it implies */
  --avail-w: min(calc(100vw - 2 * var(--stage-pad)), var(--max-w));
  --col-from-w: calc((var(--avail-w) - (var(--cols) - 1) * var(--gap)) / var(--cols));
  --h-cap: calc(var(--rows) * var(--col-from-w) * var(--ar-inv) + (var(--rows) - 1) * var(--gap));

  /* The stage's height, and the grid width that height implies. --stage-chrome
     is the pitch line and the CTA, measured by grid.js — subtracting it is what
     stops the title from taking space the grid has already spent. */
  --stage-h: calc(100dvh - var(--hud-h) - var(--dock-h)
                  - 2 * var(--stage-pad-y) - var(--stage-chrome));
  --row-from-h: calc((var(--stage-h) - (var(--rows) - 1) * var(--gap)) / var(--rows));
  --w-cap: calc(var(--cols) * var(--row-from-h) * var(--ar) + (var(--cols) - 1) * var(--gap));

  display: grid;
  grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
  gap: var(--gap);
  margin-inline: auto;
  /* A short final row centres rather than hanging off the left. Thirteen cards
     four across is 4/4/4/1, and that lone card belongs under the middle of the
     wall, not under its first column. */
  justify-content: center;
}
/* Set by grid.js on a short final row: how many columns to slide it right so
   it sits under the middle of the wall. Half-column values are the whole point
   — one card left over from four wants 1.5 — which a grid line cannot express
   and a transform can. */
.slot[style*='--shift'] { transform: translateX(calc(var(--shift) * (100% + var(--gap)))); }

.slot { position: relative; min-width: 0; min-height: 0; }

/* ── a filled channel ─────────────────────────────────────────────────────
 * A HORIZONTAL card: circle on the left, name and what-it-is on the right,
 * stack chips underneath. Two reasons, and the second is the important one.
 *
 * It is the shape in the reference. And it is the fix for the clipping bug: the
 * old vertical stack put art, label, count and a hover button in a column, and
 * at 1440x900 the projects level computed a 259x195 tile whose children came to
 * exactly 195px WITH A ONE-LINE LABEL. Every title that wrapped overflowed, and
 * above 901px `body { overflow: hidden }` meant it clipped in silence instead
 * of scrolling. Sideways, the art costs nothing vertically and the text column
 * gets the whole card height to itself. */
.chan {
  position: relative;
  width: 100%; height: 100%;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: clamp(10px, 1.4vw, 18px);
  padding: clamp(10px, 1.4vh, 18px) clamp(12px, 1.4vw, 20px);
  text-align: left;
  font: inherit; text-decoration: none;
  color: var(--card-ink);
  background: var(--card-tint);
  background-image: var(--card-sheen);
  /* the thick rule is the whole reason these read as old hardware rather than
     as flat cards; it is drawn as a border so the sheen stays inside it */
  border: 3px solid var(--card-rule);
  border-radius: var(--r-tile);
  cursor: pointer;
  overflow: hidden;
  /* TRANSFORM ONLY. The lift shadow and the ring are PAINT properties —
     transitioning them repaints a 330px tile plus a 30px blur every frame, and
     hovering is what happens immediately before every click. They sit on
     ::after and cross-fade with opacity, which the compositor does for free.
     This rule was documented here, then broken, then restored. */
  transition: transform var(--d-mid) var(--ease);
}

/* the hover state, pre-rendered and faded in rather than animated */
.chan::after {
  content: '';
  position: absolute; inset: 0;
  border-radius: inherit;
  box-shadow: 0 0 0 3px var(--ground), 0 0 0 6px var(--card-rule),
              0 14px 30px rgb(0 0 0 / .28);
  opacity: 0; pointer-events: none;
  transition: opacity var(--d-mid) var(--ease);
}

/* Lift and ring, not an inversion — flipping the face to paper was the other
   half of what read as reversed. */
.chan:hover, .chan:focus-visible { transform: translateY(-5px); }
.chan:hover::after, .chan:focus-visible::after { opacity: 1; }
.chan:focus-visible { outline: 3px solid var(--card-ink); outline-offset: -7px; }
.chan:active { transform: translateY(-1px); }

/* The circle. This is the one element carrying weight, the way the reference's
   profile photos do — inked, so it reads at a glance from across the wall. */
.chan__art {
  display: grid; place-items: center;
  width: clamp(38px, 6.2vh, 58px); height: clamp(38px, 6.2vh, 58px);
  border-radius: 50%;
  color: var(--card-tint);
  background: var(--card-ink);
}
.chan__art .ico { width: 56%; height: 56%; stroke-width: 1.9; }

/* min-width:0 on a grid child is what lets the text actually shrink instead of
   forcing the column wider than the card and pushing the circle off the edge */
.chan__text { min-width: 0; display: grid; gap: 3px; }

/* At the ROOT the card is a name and its glyph, centred as one group. Inside a
   channel it stays left-aligned: thirteen cards scanned down a column read far
   faster off a shared left edge, and those cards still carry a folder and a
   stack that the eye needs to line up. Two levels, two jobs. */
body:not(.is-sub) .chan {
  grid-template-columns: auto auto;
  justify-content: center;
}
body:not(.is-sub) .chan__label {
  font-size: 18px;                 /* stepped below; the name is alone now */
  letter-spacing: .1em;
  -webkit-line-clamp: 2;
}

.chan__label {
  font-family: var(--font-pix);
  font-size: 13px;                         /* stepped below; see the reflow block */
  letter-spacing: .07em; text-transform: uppercase; line-height: 1.25;
  color: inherit;
  /* Long repository names are one unbroken token. Without this they either
     force the card wider or spill straight out of it. */
  overflow-wrap: anywhere;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}
/* What this actually IS — the reference puts a plain descriptor under every
   name, and it is the difference between a wall of labels and a wall that
   tells you what you are looking at. */
.chan__count {
  font-size: var(--fs-micro);
  color: inherit; opacity: .72; line-height: 1.35;
  /* Two lines, not one. The pixel face is monospace and runs wider per
     character, so a descriptor that fitted on one line in the rounded face now
     truncates — "Things I wanted to exist, so I built…" was cut mid-sentence.
     The root cards have the vertical room and the fit gate checks the rest. */
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}

/* min-height, not height: the row is always in the DOM so every card's text
   block has the same number of rows and their names line up, whether or not
   there are chips to put in it. */
.chan__chips {
  display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px;
  overflow: hidden; min-height: 19px; max-height: 20px;
}
.chan__chip {
  padding: 2px 7px;
  font-family: var(--font-pix); font-size: 10px; letter-spacing: .04em;
  line-height: 1.5; white-space: nowrap;
  color: inherit;
  border: 1.5px solid currentColor; border-radius: var(--r-pill);
  opacity: .8;
}
/* Below this the card is too short to hold three rows honestly, and a chip row
   that half-shows is worse than no chip row. Hidden with display, not opacity,
   so it stops taking height as well as ink. */
@media (max-height: 780px), (max-width: 1100px) {
  .chan__chips { display: none; }
}

/* All three the same face. The icon and the name carry the difference. */
.chan[data-card='projects'] { --card-tint: var(--card-projects); --card-ink: var(--card-projects-ink); }
.chan[data-card='builds']   { --card-tint: var(--card-builds);   --card-ink: var(--card-builds-ink); }
.chan[data-card='resume']   { --card-tint: var(--card-resume);   --card-ink: var(--card-resume-ink); }

/* ── hover description ────────────────────────────────────────────────────
 * Hangs BELOW the card. Speaks the card's language now — same 3px rule, same
 * radius — rather than the soft pastel-and-blur pill it was, which was the last
 * element on the page still using the vocabulary the identity tile was
 * rejected for. It is also no longer load-bearing: every card carries its own
 * descriptor, so this is elaboration rather than the only place the text
 * lives. */
.bubble {
  position: absolute;
  left: 50%; top: calc(100% + 10px);
  transform: translate(-50%, -6px);
  z-index: 9;
  width: max-content; max-width: 320px;
  padding: 9px 14px;
  background: var(--card-projects); color: var(--card-projects-ink);
  border: 3px solid var(--card-rule);
  border-radius: var(--r-sm);
  font-size: var(--fs-small); line-height: 1.4; text-align: center;
  opacity: 0; pointer-events: none;
  transition: opacity var(--d-fast) var(--ease), transform var(--d-fast) var(--ease);
}
.bubble::after {
  content: ''; position: absolute; bottom: 100%; left: 50%;
  transform: translateX(-50%);
  border: 7px solid transparent; border-bottom-color: var(--card-rule);
}
.chan:hover + .bubble,
.chan:focus-visible + .bubble { opacity: 1; transform: translate(-50%, 0); }

.loading {
  position: absolute; inset: 0;
  display: grid; place-items: center; margin: 0;
  font-family: var(--font-pix); font-size: var(--fs-small);
  letter-spacing: .1em; color: var(--ink-3);
}
.loading[hidden] { display: none; }

/* ── bottom bar ───────────────────────────────────────────────────────────
 * The reference bar: a white band across the foot with a thin cyan hairline
 * along its top edge, circular buttons, and the clock centred in it.
 *
 * The reference has two unlabelled circles. This has five labelled ones —
 * circles for the feel, labels because the bar has to say what it does. */
.dock {
  position: relative; z-index: 3;
  flex: none;
  min-height: var(--dock-h);
  /* THREE TRACKS, not one centred row. Four buttons sit left of the clock and
     two sit right, so centring the row put the clock 80px right of the middle
     of the screen at every viewport — and the clock is what the eye reads as
     the centre of the bar. Equal 1fr tracks either side of an auto one pin it
     to the true centre no matter how many buttons flank it, which also means
     adding a seventh button cannot quietly knock it off again.
     minmax(0, 1fr): a plain 1fr is minmax(auto, 1fr) and would let an
     over-wide side group grow its own track and shove the clock across —
     re-creating the bug with extra steps. */
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  padding: 12px clamp(14px, 4vw, 46px) 14px;
  background: var(--tile);
  border-top: 3px solid var(--ink);
  border-radius: 0;
  box-shadow: none;
}

.dock__side {
  display: flex; align-items: center;
  gap: clamp(10px, 2vw, 26px);
  min-width: 0;
}
.dock__side--l { justify-content: flex-end; }
.dock__side--r { justify-content: flex-start; }
/* Back sits in the corner of the bar; everything else keeps hugging the clock.
   margin-right:auto rather than a fourth grid track, so the clock's centring is
   completely untouched by it. */
.dock__side--l .dock__btn--back { margin-right: auto; }

.dock__btn {
  position: relative;
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  background: none; border: 0; padding: 0;
  color: var(--ink-2); cursor: pointer; text-decoration: none;
  transition: transform var(--d-fast) var(--ease), color var(--d-fast) var(--ease);
}
/* the circle is the button; the label sits under it */
.dock__btn::before {
  content: '';
  position: absolute; top: 0; left: 50%;
  width: 54px; height: 54px;
  transform: translateX(-50%);
  border-radius: 50%;
  background: var(--tile);
  border: 2px solid var(--ink);
  box-shadow: none;
  transition: border-color var(--d-fast) var(--ease), background var(--d-fast) var(--ease);
}
.dock__btn .ico {
  position: relative;
  width: 26px; height: 26px;
  margin: 14px 14px 0;
}
.dock__label {
  position: relative;
  font-size: 12px; font-weight: 800; letter-spacing: .02em;
  line-height: 1; white-space: nowrap; color: var(--ink-2);
  margin-top: 12px;
}
.dock__btn:hover { transform: translateY(-3px); color: var(--ink); }
.dock__btn:hover::before { background: var(--ink); }
.dock__btn:hover .dock__label { color: var(--ink); }
.dock__btn:focus-visible { outline: none; }
.dock__btn:focus-visible::before {
  border-color: var(--focus-core);
  box-shadow: 0 0 0 4px var(--focus-halo);
}

/* the clock, centred in the bar — the strongest single cue in the reference */
.dock__clock {
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  margin-inline: clamp(6px, 2.5vw, 34px);
  user-select: none;
}
.dock__clock .clock {
  font-family: var(--font-pix);
  font-size: 22px;      /* stepped, never clamped */
  color: var(--ink-3);
  letter-spacing: .04em;
  font-variant-numeric: tabular-nums;
}
.dock__date {
  font-family: var(--font-pix);
  font-size: var(--fs-small);
  color: var(--ink-3);
  letter-spacing: .06em;
}

/* ── boot overlay ────────────────────────────────────────────────────── */
.boot {
  position: fixed; inset: 0; z-index: 1500;
  display: grid; place-items: center;
  width: 100%; border: 0; margin: 0; padding: 0;
  background: #000;
  color: #fff;
  cursor: pointer;
  font: inherit;
  transition: opacity var(--d-slow) var(--ease);
}
.boot[hidden] { display: none; }
.boot.is-going { opacity: 0; pointer-events: none; }

.boot__inner {
  display: grid; justify-items: center; gap: 0; text-align: center;
  padding: 24px; max-width: 46rem;
}

/* Laid out like a hardware warning notice: a heading with a triangle, body copy
   in caps, a URL, then the prompt. The words are ours — publish.sh fails the
   build on the console trademark strings, so borrowing the text was never an
   option even before taste came into it. */
.boot__head {
  display: flex; align-items: center; gap: 12px;
  font-size: 22px; font-weight: 800; letter-spacing: .01em;
  animation: bootIn var(--d-slow) var(--ease) both;
}
.boot__warn { width: 1em; height: 1em; flex: none; fill: #fff; stroke: none; }
.boot__warn-bar { fill: #000; }

.boot__body {
  margin-top: 34px;
  font-size: 14px; line-height: 1.85; letter-spacing: .02em;
  color: #e9e9e9;
  animation: bootIn var(--d-slow) var(--ease) 90ms both;
}
.boot__also {
  margin-top: 34px;
  font-size: 12px; color: #cfcfcf;
  animation: bootIn var(--d-slow) var(--ease) 160ms both;
}
.boot__url {
  margin-top: 8px;
  font-size: 14px; font-weight: 800;
  animation: bootIn var(--d-slow) var(--ease) 200ms both;
}
.boot__cue {
  margin-top: 40px;
  display: flex; align-items: center; gap: 8px;
  font-size: 15px; font-weight: 800; letter-spacing: .01em;
  color: #fff;
  /* A hard step, not a soft pulse. The old 2.4s ease read as breathing; a
     prompt waiting for input blinks. */
  animation: bootIn var(--d-slow) var(--ease) 260ms both,
             blink 1.06s step-end 700ms infinite;
}
.boot__key {
  display: inline-grid; place-items: center;
  width: 1.5em; height: 1.5em;
  border: 2px solid #fff; border-radius: 50%;
  font-size: 12px; font-weight: 800; line-height: 1;
}

@keyframes bootIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes blink  { 0%, 55% { opacity: 1; } 55.01%, 100% { opacity: 0; } }

/* ── channel entrance ────────────────────────────────────────────────── */
.chan--in { animation: pop var(--d-slow) var(--ease-pop) both; animation-delay: var(--delay, 0ms); }
@keyframes pop { from { opacity: 0; transform: scale(.9) translateY(6px); } to { opacity: 1; transform: none; } }

@media (prefers-reduced-motion: reduce) {
  .chan--in { animation: none; }
  .chan:hover::before, .chan:focus-visible::before { animation: none; }
  .boot__head, .boot__body, .boot__also, .boot__url, .boot__cue { animation: none; }
}
:root[data-motion='off'] .chan--in { animation: none; }
:root[data-motion='off'] .chan:hover::before,
:root[data-motion='off'] .chan:focus-visible::before { animation: none; }
:root[data-motion='off'] .boot__head,
:root[data-motion='off'] .boot__body,
:root[data-motion='off'] .boot__also,
:root[data-motion='off'] .boot__url,
:root[data-motion='off'] .boot__cue { animation: none; }

/* ── reflow ──────────────────────────────────────────────────────────────
 * Above 900px the requirement is absolute: one screen, no vertical scroll.
 * Below it there is no honest way to fit twelve readable slots on a phone, so
 * the document scrolls and the tiles get their proportions back. */
/* Integer steps for the bitmap face. A clamp() lands on fractional pixels at
   nearly every window size and Departure Mono smears when it does. */
/* Every text size on this page is an INTEGER at every window size. Departure
   Mono is a bitmap face: a clamp() lands on a fractional pixel at nearly every
   viewport width and the glyphs smear when it does. That was survivable while
   the face was confined to labels; it is the whole site's body text now, so the
   sizes step at breakpoints instead of sliding. */
@media (min-height: 900px)  { .chan__count { font-size: var(--fs-small); } }
@media (min-width: 1280px)  { .chan__label { font-size: 15px; } .level__lead { font-size: 15px; }
                              body:not(.is-sub) .chan__label { font-size: 20px; } }
@media (min-width: 1600px)  { .chan__label { font-size: 16px; } .level__lead { font-size: 16px; }
                              body:not(.is-sub) .chan__label { font-size: 22px; } }
@media (min-width: 1200px)  { .dock__clock .clock { font-size: 24px; } }
@media (min-width: 1600px)  { .dock__clock .clock { font-size: 26px; } }
@media (min-width: 900px)   { .boot__head { font-size: 26px; } .boot__body { font-size: 15px; }
                              .boot__url  { font-size: 15px; } .boot__cue  { font-size: 16px; } }
@media (min-width: 1400px)  { .boot__head { font-size: 30px; } }

@media (min-width: 901px) {
  html, body { height: 100dvh; overflow: hidden; }
  .grid {
    /* flex-grow, not height:100% — the stage is a column with the pitch line
       and the CTA in it now, and 100% would resolve against the WHOLE stage and
       push those two straight off the bottom. Growing into what is left is the
       same intent expressed against the right box. */
    flex: 1 1 auto; min-height: 0;
    max-height: var(--h-cap);                        /* bound by the WIDTH */
    width: min(100%, var(--w-cap), var(--max-w));    /* bound by the HEIGHT, and capped */
    grid-template-rows: repeat(var(--rows), minmax(0, 1fr));
  }
}

@media (max-width: 900px) {
  .hud { height: auto; padding-block: 10px; }
  .stage { justify-content: flex-start; padding-block: 10px 20px; }
  .grid { width: 100%; }
  .slot { aspect-ratio: var(--ar); }
  /* Two columns, not four. Thirteen cards four across on an 820px tablet
     computes a 182 x 76 card — too small to hold a circle and two lines of
     type, which is the bug this whole change exists to stop shipping.
     !important because grid.js writes --cols inline from the item count, and an
     inline style outranks a stylesheet rule. */
  .grid { --cols: 2 !important; }
  /* grid.js computed the shift for its own column count; with the breakpoint
     overriding --cols the offset is meaningless and would slide cards off the
     edge. The last row simply hangs left here. */
  .slot[style*='--shift'] { transform: none; }
}

@media (max-width: 600px) {
  .stage { padding-inline: 8px; }
  /* One column of wide banners. Two columns would strand RÉSUMÉ at half width,
     and an empty socket reads as broken rather than deliberate once there is no
     grid shape left for it to belong to. */
  .grid { --cols: 1 !important; --gap: 12px; }
  .bubble { display: none; }        /* no hover on touch; the label carries it */
  .chan__art { width: 42px; height: 42px; }
  /* Back to a wrapping row on a phone. Three tracks needs the clock between two
     groups, and the clock is hidden here — so the middle track would collapse
     to nothing and the two side groups would sit apart with a hole between
     them. Centring the row is correct once there is no clock to centre ON. */
  .dock {
    display: flex; flex-wrap: wrap; justify-content: center;
    gap: 5px; padding: 6px 6px 12px;
  }
  .dock__side { flex-wrap: wrap; justify-content: center; gap: 5px; }
  .dock__btn { min-width: 66px; min-height: 56px; padding: 8px 8px 7px; }
  .dock__label { font-size: 11px; }
  /* The whole clock block, not just the time. Hiding only .clock left the date
     behind — a bare "Fri 8/7" adrift between the buttons of a wrapped bar,
     which is neither useful nor centred nor deliberate-looking. */
  .dock__clock { display: none; }
}



.noscript { max-width: 46rem; margin: 40px auto; padding: 0 20px; }
