/* ============================================================
   TERMINAL MODULE - workstation 01, "The Computer".

   Two states, one DOM:

   1. AT REST (no JS, no GSAP, or prefers-reduced-motion): a full width
      black band in the flow, every line readable, the three buttons
      usable. That is the base of this file.

   2. SCENE (class .term--scene, set by js/terminal.js when GSAP and
      ScrollTrigger are present and motion is allowed): the band is
      pinned, opens until it covers the whole screen, then the typing
      runs on scroll. Only clip-path, transform and opacity move.

   The page background and the topbar turn to ink through html.term-dark,
   set by the JS: no topbar rule is edited at the source. The fades that
   go with it are scoped to html.term-fx, a class the JS only sets when
   the scene actually runs (section 5).

   COMPOSITION (polish pass). The listing is measured in `ch`, so the
   whole block scales with one number, --term-fs:

     8ch  ref        REF-001                (7 characters)
     1fr  label      up to 43 characters
     21ch stack      next.three.js.prisma   (20 characters)
     14ch status     [IN PROGRESS]          (13 characters)
     2.6ch arrow     the "open" hint, shown on hover / focus

   plus 4 gutters of 1.8ch and 1.2ch of row padding, so a row needs
   about 98ch and the block is capped at 104ch: --term-fs is tuned at
   each breakpoint so that 104ch always fits between the left margin
   (the page content column) and the right margin. Keep that invariant
   if you touch the sizes: a REF row must never wrap on a desktop.
   ============================================================ */

.term {
  /* Scene ink: darker than --ink so a full screen of it reads as black. */
  --term-ink: #0f181e;
  --term-bar-bg: #0a1015;
  --term-hair: rgba(169, 198, 217, 0.14);
  --term-fg: #f4f7f9;      /* porcelain */
  --term-dim: #a9c6d9;     /* powder blue */
  --term-brass: #c79b56;   /* lightened brass: readable on the ink */
  --term-green: #8ac290;   /* lightened green, same hue as the original */

  --term-top: 0px;         /* topbar height, measured in JS */
  --term-bar-h: 44px;

  /* Left margin = left edge of the page content column (.poste > * is
     capped at --maxw and padded), so the black scene is composed on the
     same vertical as the rest of the page instead of hugging the edge. */
  --term-gut: clamp(1.1rem, 4vw, 3rem);
  --term-pad-x: max(var(--term-gut), calc(50vw - var(--maxw) / 2 + var(--term-gut)));
  --term-pad-r: var(--term-pad-x);
  --term-pad-y: clamp(1rem, 3.4vh, 2.25rem);

  /* Usable width of the listing: 104 mono columns, so it follows the type
     size. Everything is left aligned on --term-pad-x, title bar included. */
  --term-w: min(100%, 104ch);

  /* Driven by js/terminal.js. The clip and shift of the first wave are
     gone with the band: the sheet now rises on plain scroll. */
  --term-arm-o: 1;         /* the waiting caret, on from the first frame */
  --term-scroll: 0px;      /* scrollback: how far the block has run up */
  --term-scrolled: 0;      /* 0 to 1, fade at the top edge while it scrolls */
  --term-drift: 0px;       /* slow residual travel during the final hold */
}

/* ---------- 1. Full width band (cancels .poste > *) ---------- */

.poste .term {
  max-width: none;
  margin: clamp(1.5rem, 4vw, 2.75rem) 0 0;
  padding-left: 0;
  padding-right: 0;
  /* The scene leaves the flow and is pinned: the containment that comes
     with content-visibility would create a containing block and skip the
     rendering. Cancelled here. */
  content-visibility: visible;
  contain-intrinsic-size: auto;
}

.term-scene {
  position: relative;
  background: var(--term-ink);
}

.term-window {
  position: relative;
  max-width: none;
  margin: 0;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  background: transparent;
  overflow: hidden;
}

.term-bar {
  padding: 0.6rem var(--term-pad-r) 0.6rem var(--term-pad-x);
  background: var(--term-bar-bg);
  border-bottom: 1px solid var(--term-hair);
}
.term-titlebar { color: var(--term-dim); }

.term-body {
  background: transparent;
  color: var(--term-fg);
  padding: clamp(1.2rem, 3.4vw, 2rem) var(--term-pad-r) clamp(1.3rem, 3.4vw, 2.1rem) var(--term-pad-x);
}

.term-lines {
  width: var(--term-w);
  margin-inline: 0;
}

.term-prompt { color: var(--term-brass); }
.term-ref { color: var(--term-brass); }
.term-stack { color: var(--term-dim); }
.term-caret { background: var(--term-dim); }

.term-out { margin: 0 0 0.75rem; }
.term-hint {
  margin: 0.15rem 0 0;
  color: color-mix(in srgb, var(--term-dim) 62%, transparent);
}
.term-cmd-open { margin-top: 0.9rem; }

/* Status tags keep the colours of the original, lifted just enough to
   stay readable on the scene ink. */
.term-status.is-ongoing,
.term-status.is-progress { color: var(--term-brass); }
.term-status.is-delivered { color: var(--term-green); }

/* ---------- 2. Ghost and live line ----------
   The ghost carries the full text: it reserves the space AND stays in the
   accessibility tree (opacity, not visibility). The live line sits exactly
   on top of it, so it breaks at the same places. */

.tline {
  position: relative;
  display: block;
  min-width: 0;
}
.tline-ghost { display: block; }
.tline-live { display: none; }

/* ---------- 3. REF entries ----------
   Columns in ch, identical from one row to the next: the four columns line
   up like real `ls` output whatever the length of the label (the label
   column alone absorbs a shortage of room). */

.term-entry {
  position: relative;
  grid-template-columns: 8ch minmax(0, 1fr) 21ch 14ch 2.6ch;
  column-gap: 1.8ch;
  align-items: start;
  padding: 0.34rem 0.6ch;
  border-radius: 8px;
}
.term-desc { color: var(--term-fg); }

/* "open" hint at the end of the row. Its column is reserved at all times,
   so showing it moves nothing. */
.term-go {
  grid-column: 5;
  justify-self: end;
  color: var(--term-brass);
  opacity: 0;
  transform: translateX(-0.4ch);
  transition: opacity 0.16s ease, transform 0.16s ease;
}
@media (hover: hover) and (pointer: fine) {
  .term-entry:hover .term-go { opacity: 1; transform: none; }
}
.term-entry:focus-visible .term-go { opacity: 1; transform: none; }

.term-entry:focus-visible {
  outline: 2px solid var(--term-dim);
  outline-offset: 2px;
}

/* ---------- 4. Full screen scene ----------

   ENTRANCE (rewritten twice). The panel is not clipped open from a band in
   the middle of the page, and it does not rise from the bottom edge either:
   the block is exactly one viewport tall, and over the screen of scroll that
   separates the workstation panel from the pin, js/terminal.js (section 6c)
   pushes the camera INTO the laptop drawn on that panel until its screen is
   the viewport. The pin then takes over. Section 4ter below holds the few
   rules that entrance needs; everything else here describes the panel once
   it fills the screen, which is where the zoom lands. js/terminal.js adds a
   veil behind it so the page darkens while it is covered. */

.term--scene {
  position: relative;
  z-index: 1;               /* below the topbar (50): the nav stays reachable */
  height: 100vh;
  height: 100svh;
  min-height: 100svh;
  margin: 0;
  overflow: visible;
  /* Tuned against the 104ch invariant of the header comment:
     1280 -> 18.2px, 1440 -> 19.5, 1600 -> 20.8, 1920 -> 23.4, cap 24. */
  --term-fs: clamp(1rem, 0.5rem + 0.8vw, 1.5rem);
  --term-pad-r: clamp(1.1rem, 2.6vw, 3rem);
}

/* The scene block sits straight under the workstation panel: no gap, so the
   zoom starts on the very frame the station pin releases. The margin stays
   for the static band of the fallbacks, which is a block of the page and
   needs its air. (`.poste .term` in section 1 is more specific than
   `.term--scene`, hence the selector.) */
.poste .term.term--scene { margin-top: 0; }

/* Background guard under the panel: one viewport of ink with a 1 px bleed,
   so no hairline of paper can show at the seams. */
.term--scene::before {
  content: "";
  position: absolute;
  top: -1px;
  left: 0;
  right: 0;
  height: calc(100svh + 2px);
  background: var(--term-ink);
  pointer-events: none;
}

/* Exactly one viewport, stuck to the top of the block: if the block is ever
   taller than a screen the panel still measures one screen. */
.term--scene .term-scene {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 100vh;
  height: 100svh;
}

.term--scene .term-window {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

/* --term-top is the height of the page topbar, but only once the topbar
   actually covers the panel: while the sheet rises its own title bar sits on
   its top edge, and it slides down under the topbar exactly as the two meet.
   The value is computed in JS from the entrance progress, no layout read. */
.term--scene .term-bar {
  position: absolute;
  top: var(--term-top);
  left: 0;
  right: 0;
  height: var(--term-bar-h);
  padding-top: 0;
  padding-bottom: 0;
}

.term--scene .term-body {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--term-top) + var(--term-bar-h));
  bottom: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* `safe`: when the session is longer than the room available (the SSH
     script on a laptop, a phone in landscape) it sticks to the top and the
     scrollback below takes over instead of overflowing both ends. */
  justify-content: safe center;
  /* Optical centre: a touch of extra room under the block so the eye reads
     it as centred rather than sitting low. */
  padding: var(--term-pad-y) var(--term-pad-r) calc(var(--term-pad-y) + 2vh) var(--term-pad-x);
  font-size: var(--term-fs);
  line-height: 1.9;
  overflow: hidden;
}

/* Scrollback. A real terminal longer than its window runs its earlier lines
   up and out of view: --term-scroll is the offset the JS derives from the
   line being written, --term-drift the slow residual travel during the final
   hold so the finished screen never looks frozen. */
.term--scene .term-lines {
  transform: translate3d(0, calc(-1 * (var(--term-scroll) + var(--term-drift, 0px))), 0);
  will-change: transform;
}

/* Fade at the top edge while the scrollback runs, so lines dissolve instead
   of being sliced by the clip. Painted above the text, never clickable. */
.term--scene .term-body::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: calc(var(--term-pad-y) + 2.6em);
  background: linear-gradient(to bottom,
    var(--term-ink) 0%,
    var(--term-ink) 42%,
    color-mix(in srgb, var(--term-ink) 0%, transparent) 100%);
  opacity: var(--term-scrolled);
  pointer-events: none;
  z-index: 2;
}

/* Paragraph rhythm follows the type, not the root font size. */
.term--scene .term-out { margin: 0 0 0.5em; }
.term--scene .term-cmd { margin: 0 0 0.5em; gap: 0.6em; }
.term--scene .term-cmd-open { margin-top: 0.9em; }
.term--scene .term-hint {
  margin-top: 0.35em;
  font-size: 0.92em;
  letter-spacing: 0.01em;
}

.term--scene .tline-ghost { opacity: 0; }
.term--scene .tline-live {
  display: block;
  position: absolute;
  inset: 0;
}

/* The prompt only shows up with its own line. */
.term--scene .term-cmd .term-prompt {
  opacity: 0;
  transition: opacity 0.12s linear;
}
.term--scene .term-cmd.is-live .term-prompt { opacity: 1; }

/* Rows of the listing. They bleed 0.6ch past the text column on both
   sides, so the highlight has air while REF-001 stays aligned under the
   `$` of the command above it. */
.term--scene .term-list { margin-inline: -0.6ch; }
.term--scene .term-entry {
  column-gap: 1.8ch;
  padding: 0.42em 0.6ch;
  margin: 0.1em 0;
  pointer-events: none;     /* clickable only once written */
  transition: background-color 0.16s ease;
}
.term--scene .term-entry.is-live { pointer-events: auto; }
.term--scene .term-desc {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}
.term--scene .term-ref { grid-column: 1; }

/* Brass rule on the active row: the cursor of a file manager. */
.term--scene .term-entry::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.12em;
  bottom: 0.12em;
  width: 2px;
  border-radius: 2px;
  background: var(--term-brass);
  opacity: 0;
  transition: opacity 0.16s ease;
}
.term--scene .term-entry:focus-visible::before { opacity: 1; }
@media (hover: hover) and (pointer: fine) {
  .term--scene .term-entry:hover { background: color-mix(in srgb, var(--term-dim) 13%, transparent); }
  .term--scene .term-entry:hover::before { opacity: 1; }
}

/* Writing caret, moved into the current line by the JS.
   ZERO width, the visible block is painted by ::after as an overflow:
   otherwise the caret counts in the line box and can push it to a second
   line exactly when that line finishes being written. */
.term-write {
  position: relative;
  display: inline-block;
  width: 0;
  height: 1.02em;
  vertical-align: baseline;
}
.term-write::after {
  content: "";
  position: absolute;
  left: 0.08em;
  bottom: -0.16em;
  width: 0.58em;
  height: 1.02em;
  background: var(--term-dim);
  animation: term-blink 1.05s steps(1, end) infinite;
}
/* During the beat of pure black the caret arrives on its own, before the
   first character. --term-arm-o is driven by the JS and stays at 1 for the
   whole typing phase. */
.term--scene .term-write::after { opacity: var(--term-arm-o); }

/* Static band: the caret closes the `open` command. */
.term-caret-static { margin-left: -0.42rem; }
.term--scene .term-caret-static { display: none; }

/* ============================================================
   4ter. THE ZOOM INTO THE LAPTOP SCREEN

   A handful of rules, all of them switched on by js/terminal.js (section 6c)
   and, bar the first, only while the push runs. The geometry itself is two
   matrices written by the JS from one measured quadrilateral; nothing about
   the composition is decided here.

   `html.term-zoom` is set for 0 < progress < 1 only, so at both ends of the
   window the page is exactly what the rest of this file describes: the
   workstation panel in the flow, then the pinned session.
   ============================================================ */

/* The panel is blown up to about four screens across while the camera comes
   in, so it overflows the page sideways. `clip`, not `hidden`: it makes no
   scroll container, and it does not touch the two pinned panels, whose
   containing block is the viewport. Declared as soon as the scene is armed
   rather than with the class, so no style change can land mid scrub. */
html.term-fx #computer.poste { overflow-x: clip; }

/* The panel travels on the pin spacer ScrollTrigger gave the workstation
   header: the only box of that scene nothing else writes a transform to, so
   js/stations.js keeps every transform it owns. The class is put on the
   spacer by js/terminal.js, never by the stylesheet, and `will-change` only
   exists while the push runs: on a transformed ancestor a pinned, fixed
   child would lose the viewport as its containing block. */
html.term-zoom .term-zoom-stage {
  transform-origin: 0 0;
  will-change: transform;
}

/* The session's panel is the frame the push lands in. It keeps the absolute
   box section 4 gives it (full width, one small viewport tall, at the top of
   its block), so the hand-off at the end of the zoom moves nothing: the JS
   matrix simply carries the block's own travel down the screen. It is NOT
   made `fixed` here, although that would read more naturally: ScrollTrigger
   puts an identity `transform` on every element it pins, and that makes the
   pinned block the containing block of any fixed child of it. */
html.term-zoom .term--scene .term-scene {
  z-index: 2;
  transform-origin: 0 0;
  will-change: transform, opacity;
  /* Nothing of the session is live before the pin: the REF rows only become
     buttons once they are written. */
  pointer-events: none;
}

/* The four things that fade during the push, promoted for its duration so
   their fades are composited instead of repainting a sheet of sixty paths
   and a block of text on every frame. Only while the class is on: a layer
   per element for the whole life of the page would cost memory for nothing.
   `will-change: filter` does make its element the containing block of a
   fixed child, which is why it is only on the cartouche: the two panels that
   ScrollTrigger pins are not inside it. */
html.term-zoom #computer .poste-svg-pc,
html.term-zoom .term--scene .term-window { will-change: opacity; }
html.term-zoom #computer .poste-intro { will-change: opacity, filter; }
html.term-zoom #computer .stn-screen,
html.term-zoom #computer #pc-code-b { will-change: opacity; }

/* The ink guard of section 4 sits at the block's own place in the flow,
   which during the push is a band climbing from the bottom edge: exactly the
   move the zoom replaces. It comes back the frame the panel is pinned. */
html.term-zoom .term--scene::before { display: none; }

/* ============================================================
   4bis. THE SSH SESSION

   Two scripts share this markup (see js/terminal.js): SCRIPT_SSH, which
   runs by default, and SCRIPT_SHORT, the earlier whoami + ls session. Every
   line carries data-step; the JS switches off the steps that do not belong
   to the script it runs. `term-off` in index.html is only the no-JS default.
   ============================================================ */

.term-off { display: none !important; }
/* Blocks that only make sense while the scene plays. In the static band
   (no JS, no GSAP, reduced motion) the session is cut back to its essential
   lines: ssh, the handshake, whoami, the listing and the closing prompt. */
.term:not(.term--scene) .term-only-scene { display: none !important; }

/* ---------- prompts ----------
   user@host green, path powder blue, the sigil brass: the three accent
   tokens already declared at the top of this file, nothing new. */
.term-prompt { flex: 0 0 auto; white-space: nowrap; }
.tp-u { color: var(--term-green); }
.tp-p { color: var(--term-dim); }
.tp-s { color: var(--term-brass); }

/* Title bar: it follows the session, felix@laptop before the ssh line has
   landed, felix@vps after. Both labels sit in the same grid cell, so the
   swap is a cross fade and moves nothing. */
.tb-swap { display: inline-grid; }
.tb-a,
.tb-b { grid-area: 1 / 1; }
.term:not(.term--scene) .tb-a { display: none; }
.term--scene .tb-a,
.term--scene .tb-b { transition: opacity 0.35s ease; }
.term--scene .tb-b { opacity: 0; }
.term--scene.is-remote .tb-a { opacity: 0; }
.term--scene.is-remote .tb-b { opacity: 1; }

/* ---------- colour of the lines ----------
   Typed commands porcelain, printed output muted, the banner a shade
   brighter, container status and the closing check green. */
.term-cmd .tline { color: var(--term-fg); }
.term-conn .tline,
.term-tbl .tline { color: color-mix(in srgb, var(--term-dim) 74%, transparent); }
.term-motd .tline { color: var(--term-dim); }
.term-tbl-head .tline { color: color-mix(in srgb, var(--term-dim) 46%, transparent); letter-spacing: 0.06em; }
.term-tbl .term-up { color: var(--term-green); }
.term-ok .tline { color: var(--term-green); }

/* Output blocks printed as one burst read as one paragraph: no gap between
   their lines, one line of air before the next command. */
.term-tight { margin: 0; }
.term-tight + .term-cmd { margin-top: 0.75em; }
.term--scene .term-tight { margin: 0; }
.term--scene .term-tight + .term-cmd { margin-top: 0.75em; }

/* `docker ps` output: two mono columns that line up like the real thing. */
.term-tbl {
  display: grid;
  grid-template-columns: 14ch max-content;
  column-gap: 0;
}

/* ---------- the loading line ----------
   REALIGNED (vague 5). The cells used to be the block glyphs U+2588 and
   U+2591. They are not in IBM Plex Mono, so they came from a fallback font
   with its own ascent: the filled blocks stood half a line above the
   brackets, the percentage and the label, which sat on the baseline. Each
   cell is now a plain box, drawn in CSS and centred on the same line as the
   text around it, one `ch` of pitch so the bar keeps its 20ch at 0 % and at
   100 %. Nothing depends on a font any more. */
/* ALIGNED ON THE BASELINE (vague 6). Centring the cells on the line box put
   them 0.9em tall around the middle of the line: in line with the text to a
   pixel, but taller than the digits and resting on no line of the type. Each
   cell is now exactly the cap height of the font (`1cap`, 0.7em where the
   unit is unknown, which is Plex Mono's 698/1000) and flex aligns every item
   on the BASELINE: a box with no text uses its bottom edge as its baseline, so
   the cells stand on the same line as the brackets, "100%" and the label, and
   reach the same height as the digits, whatever the metrics of the font.
   Measured, vague6/term5/probe-bar.js. Felix's capture 34 showed the older
   glyph cells (no gap between them, brackets touching): a stale cached
   stylesheet, since python's http.server sends no Cache-Control. */
.term-load {
  display: flex;
  align-items: baseline;
  gap: 1.4ch;
  margin: 0.1em 0 0.5em;
  transform-origin: left center;
}
.tload-cells {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: baseline;
  color: var(--term-dim);
}
.tload-cells::before { content: "["; margin-right: 0.25ch; }
.tload-cells::after { content: "]"; margin-left: 0.25ch; }
.tload-cells i {
  display: block;
  flex: 0 0 auto;
  width: 0.78ch;
  height: 0.7em;
  height: 1cap;
  margin: 0 0.11ch;
  border-radius: 1px;
  background: color-mix(in srgb, var(--term-dim) 22%, transparent);
}
.tload-cells i.is-on { background: var(--term-brass); }
.tload-pct {
  flex: 0 0 auto;
  min-width: 4.5ch;
  text-align: right;
  color: var(--term-fg);
  font-variant-numeric: tabular-nums;
}
.tload-step {
  flex: 0 0 auto;
  min-width: 19ch;
  color: var(--term-dim);
}

/* ---------- the three project files (vague 5) ----------
   The listing is what the session is for, so on a desktop each file is a
   larger two line entry: REF, label and status on the first line, the stack
   under the label, in the dim of the printed output. Everything stays on the
   ch grid of the terminal: 8 + 43 + 14 + 2.6 columns and three gutters are
   73ch of the row's own size, 94ch of the body's at 1.28, inside the 104ch
   the body is sized for. The ghosts hold the room from the first frame, so
   nothing reflows while the rows are written.

   A brass hairline draws itself across each row before its REF is typed
   (js/terminal.js, applyRows), the way the lines of the drawings arrive. */
.term-rule { display: none; }
.term--scene .term-rule {
  display: block;
  position: absolute;
  left: 0.6ch;
  right: 0.6ch;
  top: 0;
  height: 1px;
  background: color-mix(in srgb, var(--term-brass) 70%, transparent);
  transform: scaleX(0);
  transform-origin: left center;
  opacity: 0;
  pointer-events: none;
}

@media (min-width: 901px) and (min-height: 561px) {
  .term--scene .term-list { margin-top: 0.4em; }
  .term--scene .term-entry {
    font-size: 1.28em;
    grid-template-columns: 8ch minmax(0, 1fr) 14ch 2.6ch;
    grid-template-rows: auto auto;
    column-gap: 1.8ch;
    row-gap: 0;
    padding: 0.5em 0.47ch 0.55em;
    margin: 0.15em 0;
    line-height: 1.55;
  }
  .term--scene .term-ref { grid-area: 1 / 1 / 2 / 2; }
  .term--scene .term-desc { grid-area: 1 / 2 / 2 / 3; }
  .term--scene .term-status { grid-area: 1 / 3 / 2 / 4; }
  .term--scene .term-go { grid-area: 1 / 4 / 2 / 5; }
  .term--scene .term-stack {
    grid-area: 2 / 2 / 3 / 4;
    font-size: 0.7em;
    letter-spacing: 0.02em;
    color: color-mix(in srgb, var(--term-dim) 82%, transparent);
  }
}

/* ---------- the three project files in ASCII frames (vague 6) ----------
   Built by js/terminal.js (section 0c) whenever it runs, the scene or not:
   the no-JS band keeps the rows above. One frame per file, traced on the
   character grid of the text (1ch by one line), its number in figlet
   letters, its name, line, stack and status inside. The double line is SVG
   because the box drawing glyphs are not in IBM Plex Mono (see the JS).

   Two layouts:
     - wide (1100 px and up): three tall frames side by side, the moment the
       section is for;
     - narrow: three wide frames stacked, the digits on the left of the text,
       so a phone gets them in about half the height. */
.term-list.has-box {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0.9em;
  margin: 0.5em 0 0.4em;
}
.term--scene .term-list.has-box { margin-inline: 0; }
.term-list.has-box .term-row { min-width: 0; display: flex; }
.term-list.has-box .term-entry.has-box,
.term--scene .term-list.has-box .term-entry.has-box {
  display: flex;
  align-items: stretch;
  flex: 1 1 auto;
  width: 100%;
  padding: 0;
  margin: 0;
  font-size: 1em;
  line-height: inherit;
  border-radius: 0;
  background: transparent;
}
.term-entry.has-box > :not(.tbox) { display: none !important; }
.term--scene .term-entry.has-box::before { display: none; }

.tbox {
  --tbox-line: var(--term-brass);
  position: relative;
  flex: 1 1 auto;
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr) max-content;
  grid-template-areas:
    "ref  ref   status"
    "fig  name  name"
    "fig  line  line"
    "fig  stack stack";
  align-items: start;
  column-gap: 2.4ch;
  /* the frame takes the outer ring of cells: one line, two columns */
  padding: 1.5em 1.2em;
  line-height: 1.5;
  font-size: 1em;
  color: var(--term-fg);
  text-align: left;
  transition: background-color 0.16s ease;
}
.tbox-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  fill: none;
  stroke: var(--tbox-line);
  stroke-linecap: butt;
  pointer-events: none;
}
.tbox-sep-l { stroke: color-mix(in srgb, var(--tbox-line) 55%, transparent); }
.tbox-sep { display: none; grid-area: sep; height: 1.5em; }

.tbox-ref {
  grid-area: ref;
  color: color-mix(in srgb, var(--term-dim) 80%, transparent);
  letter-spacing: 0.14em;
}
.tbox-status {
  grid-area: status;
  justify-self: end;
  font-weight: 600;
  letter-spacing: 0.02em;
  white-space: nowrap;
  color: var(--term-brass);
}
.tbox-status.is-delivered { color: var(--term-green); }
.tbox-fig {
  grid-area: fig;
  display: block;
  font-size: 1em;
  line-height: 1;
  color: var(--term-brass);
  padding-top: 0.2em;
}
.tbox-fig-l {
  display: block;
  height: 1em;
  white-space: pre;
}
.tbox-name {
  grid-area: name;
  font-size: 1.3em;
  line-height: 1.35;
  font-weight: 500;
  color: var(--term-fg);
  margin-top: 0.15em;
}
.tbox-line { grid-area: line; color: color-mix(in srgb, var(--term-fg) 88%, transparent); }
.tbox-stack {
  grid-area: stack;
  font-size: 0.86em;
  letter-spacing: 0.02em;
  color: color-mix(in srgb, var(--term-dim) 85%, transparent);
}
.tbox .tline { min-width: 0; }

@media (hover: hover) and (pointer: fine) {
  .term-entry.has-box:hover .tbox,
  .term--scene .term-entry.has-box:hover .tbox {
    --tbox-line: var(--term-fg);
    background: color-mix(in srgb, var(--term-dim) 9%, transparent);
  }
  .term--scene .term-entry.has-box:hover { background: transparent; }
}
.term-entry.has-box:focus-visible { outline-offset: 4px; background: transparent; }
.term-entry.has-box:focus-visible .tbox { --tbox-line: var(--term-fg); }

@media (min-width: 1100px) and (min-height: 561px) {
  .term-list.has-box {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 2ch;
    margin-top: 0.6em;
  }
  .tbox {
    grid-template-columns: minmax(0, 1fr) max-content;
    grid-template-areas:
      "ref   status"
      "fig   fig"
      "sep   sep"
      "name  name"
      "line  line"
      "stack stack";
    column-gap: 1.2ch;
    align-content: start;
  }
  .tbox-fig { font-size: 1.3em; padding-top: 0.35em; }
  .tbox-sep { display: block; }
  .tbox-name { margin-top: 0; }
}

/* The preamble steps back once the files are coming: --term-focus goes from
   0 to 1 while the three rows are written (js/terminal.js). The bar line is
   dimmed through its children, the arrival of the line owns its own opacity. */
.term--scene .term-lines > p:not(.term-cmd-open):not(.term-hint):not(.term-load),
.term--scene .term-load > * {
  opacity: calc(1 - 0.55 * var(--term-focus, 0));
}

/* ---------- arrival of a printed line ----------
   Blur to focus, the Colibrimo set-piece move (blur 14 px, scale 1.08,
   y 14 px, expo.out) applied to one line at a time. The JS writes the three
   values from the scroll progress and clears the filter once the line has
   landed, so nothing stays blurred during the holds. */
.term--scene .tline-live { transform-origin: left center; }

/* ---------- the veil ----------
   A fixed sheet of ink behind the rising panel: the paper page darkens while
   the sheet covers it. Created by js/terminal.js, so it never exists without
   the scene. z-index 0 keeps it under the panel (z-index 1) and under the
   topbar (50), which darkens on its own through html.term-dark. */
.term-veil {
  position: fixed;
  inset: 0;
  z-index: 0;
  background: var(--term-ink, #0f181e);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  will-change: opacity;
}
.term-veil.is-on { visibility: visible; }

/* ---------- 5. Page in ink during the scene ---------- */

/* The sticky topbar has been removed from the site. html.term-fx only ever
   carried its paper-to-ink transitions, and html.term-dark only styled the
   bar, its brand, its links, its burger and its status pill: all of that is
   gone with the bar. html.term-dark keeps the two rules below, which belong
   to elements that are still there, and js/terminal.js keeps setting both
   classes. */

/* Reticle: readable on the black. Every one of these selectors ends on a
   class, so toggling html.term-dark only invalidates the four elements that
   carry those classes. That matters since the zoom: the class is now set in
   the middle of a scrub, and a selector ending on a bare pseudo-class
   (`html.term-dark :focus-visible`, which this rule set used to have) makes
   the toggle invalidate the style of the whole document. Measured at CPU x4:
   one 60 to 100 ms task on the frame it flipped, gone once the focus rule
   below stopped depending on the class. */
html.term-dark .reticle-h,
html.term-dark .reticle-v { background: rgba(169, 198, 217, 0.7); }
html.term-dark .reticle-dot { border-color: #a9c6d9; }
html.term-dark .reticle-coord { color: rgba(169, 198, 217, 0.85); }

/* Focus rings on the ink. Not scoped to html.term-dark but to the panel
   itself: everything that can be focused over the black is inside it, and a
   rule that never changes cannot cost a recalculation. */
.term--scene :focus-visible { outline-color: #a9c6d9; }

/* ---------- 6. Hover preview ---------- */

.term-preview {
  z-index: 290;             /* above the topbar, below the reticle */
  width: auto;              /* the card follows the image */
  max-width: 280px;
  border-color: rgba(169, 198, 217, 0.22);
  background: #0a1015;
}
/* Portrait captures (phone screens) are capped in height, otherwise the
   preview covers half of the listing. */
.term-preview img {
  width: auto;
  max-width: 280px;
  max-height: 300px;
}

/* ---------- 7. Responsive ----------
   Every step below keeps the same invariant: 104ch must fit between
   --term-pad-x and --term-pad-r. */

/* Between 901 and 1140 px the content column stops growing but the screen
   does not: the type follows the width alone. */
@media (min-width: 901px) and (max-width: 1140px) {
  .term--scene { --term-fs: clamp(0.85rem, 1.5vw, 1.15rem); }
}

@media (max-width: 900px) {
  .term {
    --term-bar-h: 40px;
    --term-gut: clamp(0.7rem, 3.4vw, 1.4rem);
    --term-pad-y: clamp(0.8rem, 2.4vh, 1.5rem);
  }
  /* No room for five columns: label on the first line next to the REF,
     stack and status together on the second. Two lines, nothing else. */
  /* 7.6ch, not 7.2: .term-ref carries letter-spacing 0.02em, so REF-001 is
     7 x 0.62em = 7.23ch wide, not 7. Below that it wraps and the entry
     silently becomes three lines. */
  .term-entry {
    grid-template-columns: 7.6ch minmax(0, 1fr) max-content;
    row-gap: 0.1rem;
    column-gap: 0.8ch;
  }
  .term-ref { grid-area: 1 / 1 / 2 / 2; }
  .term-desc { grid-area: 1 / 2 / 2 / 4; }
  .term-stack { grid-area: 2 / 2 / 3 / 3; }
  .term-status { grid-area: 2 / 3 / 3 / 4; justify-self: end; }
  .term-go { display: none; }
  /* Same two lines in the static band as in the scene, so the fallback and
     the animated state read the same on a phone. */
  .term:not(.term--scene) .term-body { font-size: clamp(0.56rem, 2.9vw, 0.95rem); }

  .term--scene {
    /* Largest size that keeps a REF entry on two lines: the first one is
       7.2ch + 0.8ch + 43 characters of label, inside the row padding. */
    --term-fs: clamp(0.56rem, 2.9vw, 0.95rem);
    --term-pad-r: var(--term-pad-x);
    --term-w: 100%;
  }
  .term--scene .term-body {
    line-height: 1.7;
    padding-bottom: calc(var(--term-pad-y) + 2vh);
  }
  .term--scene .term-list { margin-inline: -0.35ch; }
  .term--scene .term-entry {
    /* 2 lines x 1.7 + padding stays above 44 px down to 320 px wide. */
    padding: 0.62em 0.35ch;
    margin: 0.3em 0;
  }
  .term--scene .term-hint { font-size: 0.95em; }
}

/* Short screens (phone in landscape): height, not width, is the constraint.
   Everything tightens so the eight lines fit. */
@media (max-height: 560px) {
  .term {
    --term-bar-h: 34px;
    --term-pad-y: 0.5rem;
  }
  .term--scene {
    --term-fs: clamp(0.62rem, 0.34rem + 0.7vw, 0.86rem);
    --term-w: 100%;
  }
  .term--scene .term-body {
    line-height: 1.45;
    padding-bottom: var(--term-pad-y);
  }
  /* The rows no longer have to be crushed to make the session fit: the
     scrollback runs the earlier lines out of view, so the three targets
     keep their 44 px even on a 390 px high landscape phone. */
  .term--scene .term-entry {
    margin: 0.1rem 0;
    padding: 0.3em 0.35ch;
    min-height: 44px;
    align-content: center;
  }
}

/* Landscape wide enough: back to the full listing, far shorter in height
   than the stacked label / stack / status. */
@media (max-height: 560px) and (min-width: 700px) {
  .term-entry {
    grid-template-columns: 8ch minmax(0, 1fr) 21ch 14ch 2.6ch;
    column-gap: 1.6ch;
    row-gap: 0;
  }
  .term-ref { grid-area: auto; }
  .term-desc { grid-area: auto; }
  .term--scene .term-stack { grid-area: auto; grid-column: 3; }
  .term--scene .term-status { grid-area: auto; grid-column: 4; justify-self: stretch; }
  .term-go { display: block; }
}

/* ---------- 8. Reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
  .term-write { animation: none; opacity: 1; }
  .term-go { transition: none; }
  .term-entry { transition: none; }
}
