/*
 * modem.dbhq.uk - plain CSS, no framework, consuming brand/tokens.css's
 * custom properties throughout. Never a hardcoded colour, type or
 * spacing value where a token exists for it - see ../brand/README.md.
 *
 * Component names below match the design spec's "Components" table and,
 * where the concept is the same, modem-tui's own naming: crt-frame,
 * terminal-line, dial-button, waterfall, phase-row, download-row,
 * endorsement.
 */

@import url("../brand/tokens.css");

/* The page container width. Not a brand/tokens.json value: that file is
   for colour, type and CRT-effect values the TUI and the page must
   agree on, and a browser container width has no terminal equivalent.
   One local constant so the hero and main line up at every viewport
   instead of two hand-matched numbers drifting apart. */
:root {
  --page-max-width: 1320px;
}

* {
  box-sizing: border-box;
}

html {
  color-scheme: dark;
}

body {
  margin: 0;
  background: var(--modem-ground);
  color: var(--modem-white-bright);
  font-family: var(--modem-type-face);
  font-size: var(--modem-type-scale-base);
  line-height: var(--modem-line-height-ratio);
}

/* Visually hidden but still reachable by assistive tech - used for the
   skip link's destination context and for text equivalents of anything
   this page otherwise conveys only visually (the glow, the waterfall). */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: -999px;
  top: 0;
  z-index: 100;
  background: var(--modem-ground);
  color: var(--modem-green-bright);
  padding: var(--modem-space-xs) var(--modem-space-sm);
  border: 1px solid var(--modem-green-bright);
}

.skip-link:focus {
  left: var(--modem-space-sm);
  top: var(--modem-space-sm);
}

/* Browser surfaces the design system otherwise leaves to the platform
   default - themed from the same phosphor rather than left grey. */
::selection {
  background: var(--modem-green-bright);
  color: var(--modem-ground);
}

* {
  scrollbar-color: var(--modem-green-dim) var(--modem-ground);
  scrollbar-width: thin;
}

*::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

*::-webkit-scrollbar-track {
  background: var(--modem-ground);
}

*::-webkit-scrollbar-thumb {
  background: var(--modem-green-dim);
  border: 2px solid var(--modem-ground);
}

a {
  color: var(--modem-green-bright);
}

a:hover,
a:focus-visible {
  color: var(--modem-white-bright);
}

:focus-visible {
  outline: 2px solid var(--modem-green-bright);
  outline-offset: 2px;
}

h1,
h2,
h3 {
  font-family: var(--modem-type-face);
  color: var(--modem-green-bright);
  font-weight: normal;
  letter-spacing: 0.02em;
}

/* Type hierarchy: h1 (the wordmark) is its own clamp; h2 and h3 step
   down from it in size only, never weight - a real terminal signals
   emphasis with size and colour, not a bold face the font stack may
   not even carry. */
h2 {
  font-size: var(--modem-type-scale-xlarge);
  margin-top: 0;
  border-bottom: 1px solid var(--modem-white-dim);
  padding-bottom: var(--modem-space-xs);
}

h3 {
  font-size: var(--modem-type-scale-large);
  margin: 0;
}

p {
  max-width: 70ch;
}

/* ---------------------------------------------------------------------
 * crt-frame - the bezel and screen the whole page sits inside. Scanlines
 * are dark stripes composited with `multiply`, so they can only darken a
 * pixel, never lighten one - lightening the gap between two already-dark
 * pixels is what would actually erode contrast. Verified against the
 * brand's own bright phosphors: even worst-case (a stripe sitting
 * directly over a glyph) leaves white-bright at 11.4:1 and green-bright
 * at 10.4:1 against the ground colour, both comfortably clear of WCAG
 * AA's 4.5:1 for body text.
 *
 * The vignette (::before) is the same idea taken to the edges of the
 * viewport: a plain black overlay composites identically whether or not
 * `multiply` is declared (multiplying anything by black is the same as
 * compositing opaque black at that alpha), so the number that matters is
 * the alpha itself. Capped at 0.3 and checked against the WCAG contrast
 * formula directly at the darkest corner stop: white-bright drops to
 * 7.8:1, green-bright to 7.2:1 - both still clear of AA's 4.5:1 even in
 * the one place the vignette is strongest. Fixed,
 * not absolute, so it reads as the screen's own edge falloff rather than
 * a mark on the document, and stays put as the long page scrolls under
 * it - the same reason the scanlines are fixed.
 * ------------------------------------------------------------------- */
.crt-frame {
  position: relative;
  min-height: 100vh;
  background: var(--modem-ground);
}

.crt-frame::before {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 49;
  background: radial-gradient(
    ellipse 85% 75% at 50% 45%,
    transparent 55%,
    rgba(0, 0, 0, 0.3) 100%
  );
}

.crt-frame::after {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 50;
  background-image: repeating-linear-gradient(
    to bottom,
    rgba(0, 0, 0, var(--modem-scanline-opacity)) 0,
    rgba(0, 0, 0, var(--modem-scanline-opacity)) 1px,
    transparent 1px,
    transparent var(--modem-scanline-pitch)
  );
  mix-blend-mode: multiply;
}

@media (prefers-reduced-motion: reduce) {
  .crt-frame::after {
    /* The pattern itself is static already - this rule exists so any
       future flicker/animation added to the overlay is disabled here,
       not discovered missing later. */
    animation: none;
  }
}

/* ---------------------------------------------------------------------
 * crt-warmup - the one authored moment of motion: the tube collapsing
 * to a bright line and expanding back out, the reverse of how a CRT
 * looks switching off. A fixed overlay painted over everything else
 * (z-index above the scanline/vignette layers), never a transform on
 * the real content - the page is full height immediately and correct
 * underneath throughout, so there is no layout to mistime.
 * ------------------------------------------------------------------- */
.crt-warmup {
  position: fixed;
  inset: 0;
  z-index: 999;
  pointer-events: none;
  background: var(--modem-ground);
  animation: modem-crt-power-on 900ms cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
}

@keyframes modem-crt-power-on {
  0% {
    transform: scaleY(0.004) scaleX(0.6);
    background: var(--modem-green-bright);
    opacity: 1;
  }
  8% {
    transform: scaleY(0.012) scaleX(1);
    background: var(--modem-green-bright);
    opacity: 1;
  }
  40% {
    transform: scaleY(1) scaleX(1);
    background: var(--modem-ground);
    opacity: 1;
  }
  100% {
    transform: scaleY(1) scaleX(1);
    background: var(--modem-ground);
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .crt-warmup {
    display: none;
  }
}

/* ---------------------------------------------------------------------
 * Hero: web/hero.png with live text over the top, per the brief - never
 * baked into the image, so it stays sharp at any size and is editable
 * without regenerating anything.
 * ------------------------------------------------------------------- */
.hero {
  position: relative;
  overflow: hidden;
  border-bottom: 1px solid var(--modem-white-dim);
}

.hero__image {
  display: block;
  width: 100%;
  /* clamp, not min: on a short viewport (a phone in landscape) 46vh
     alone can shrink below what the wordmark and tagline need, and
     .hero's own overflow: hidden would silently crop them rather than
     let them push the box taller - 14rem is a floor comfortably above
     their combined height at every viewport this clamp actually
     reaches. */
  height: clamp(14rem, 46vh, 380px);
  object-fit: cover;
  opacity: 0.55;
  /* web/hero.png was generated green at Dan's own request, and the icon
     (brand/icon.svg) is phosphor green #33FF33 - green is the brand's
     colour. An earlier pass hue-shifted this asset to amber with a CSS
     filter to make the page read as one colour system; the diagnosis
     (three competing colour systems looked accidental) was right but
     amber was the wrong fix - the asset is already the project's own
     colour, generated on purpose, so it is used as rendered. */
}

.hero__overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  background: linear-gradient(
    to top,
    rgba(10, 10, 10, 0.95) 0%,
    rgba(10, 10, 10, 0.35) 60%,
    rgba(10, 10, 10, 0.55) 100%
  );
}

/* Mirrors main's own max-width and horizontal padding, so the wordmark
   sits on the same left edge as the body copy below it however wide the
   viewport gets, while .hero__image and this gradient stay full-bleed. */
.hero__content {
  width: 100%;
  max-width: var(--page-max-width);
  margin: 0 auto;
  padding: 0 var(--modem-space-md);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--modem-space-xs);
}

.wordmark {
  margin: 0;
  font-size: clamp(2.5rem, 10vw, 5rem);
  color: var(--modem-green-bright);
  text-shadow: 0 0 var(--modem-bloom-radius) var(--modem-green-bright);
  line-height: 1;
}

.hero__tagline {
  margin: 0;
  max-width: 60ch;
  font-size: var(--modem-type-scale-large);
  color: var(--modem-white-bright);
}

/* ---------------------------------------------------------------------
 * Layout - main is wider than the 70ch reading measure `p` keeps for
 * itself (below), so wide, non-prose content (the terminal, the
 * generated frames) has room to breathe without dragging body copy out
 * to an uncomfortable line length. Top padding is deliberately tighter
 * than bottom: the hero already separates itself with its own border,
 * so main does not need to repeat that gap before the first heading.
 * ------------------------------------------------------------------- */
main {
  max-width: var(--page-max-width);
  margin: 0 auto;
  padding: var(--modem-space-md) var(--modem-space-md) var(--modem-space-lg);
}

section {
  margin-bottom: var(--modem-space-lg);
}

/* ---------------------------------------------------------------------
 * terminal-line - a line of output. The typewriter reveal (added via
 * the `.terminal-line--typing` class while a line is being animated in)
 * is purely decorative timing; the full text is present in the DOM
 * throughout; see index.html's script for why that matters for
 * assistive tech and prefers-reduced-motion.
 * ------------------------------------------------------------------- */
.terminal {
  border: 1px solid var(--modem-white-dim);
  background: var(--modem-ground);
  padding: var(--modem-space-sm);
}

.terminal__output {
  min-height: calc(var(--modem-space-lg) * 3);
  max-height: 16em;
  overflow-y: auto;
  margin: 0 0 var(--modem-space-sm) 0;
  font-size: var(--modem-type-scale-small);
}

.terminal-line {
  margin: 0;
  color: var(--modem-white-bright);
  white-space: pre-wrap;
  word-break: break-word;
}

.terminal-line--command {
  color: var(--modem-green-bright);
}

.terminal-line--phase {
  color: var(--modem-white-dim);
  padding-left: var(--modem-space-sm);
}

.terminal-line--phase::before {
  content: "- ";
}

/* The typewriter reveal used for AT responses (ATDT.../CONNECT 300).
   The full text is set on the element immediately, so a screen reader
   gets the whole line the instant it appears - this animates only the
   clip, purely cosmetic. */
.terminal-line--reveal {
  animation: modem-reveal 0.4s steps(20, end);
}

@keyframes modem-reveal {
  from {
    clip-path: inset(0 100% 0 0);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .terminal-line--reveal {
    animation: none;
  }
}

/* ---------------------------------------------------------------------
 * waterfall - the canvas and its axis furniture. Decorative: everything
 * it conveys (which phase is playing, whether there is carrier) also
 * exists as text elsewhere on the page, so it is safe to hide it from
 * assistive tech rather than attempt an ARIA description of a
 * spectrogram.
 * ------------------------------------------------------------------- */
.waterfall-wrap {
  display: flex;
  gap: var(--modem-space-xs);
  margin: var(--modem-space-sm) 0;
}

.waterfall-axis {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  font-size: var(--modem-type-scale-small);
  color: var(--modem-white-dim);
  text-align: right;
  width: 3.5ch;
  flex-shrink: 0;
}

.waterfall {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 960 / 200;
  background: var(--modem-ground);
  border: 1px solid var(--modem-white-dim);
  /* A wider native canvas (see index.html's width attribute) means more
     seconds of history are visible at once, not just a blown-up copy of
     the old one - keep upscaling crisp/blocky rather than smoothed,
     matching the rest of the CRT character-cell aesthetic. */
  image-rendering: pixelated;
}

.now-playing {
  font-size: var(--modem-type-scale-small);
  color: var(--modem-green-bright);
  min-height: 1.4em;
}

/* ---------------------------------------------------------------------
 * dial-button - the primary action, and the mic-permission action in
 * its second state.
 * ------------------------------------------------------------------- */
.demo__controls {
  display: flex;
  gap: var(--modem-space-sm);
  flex-wrap: wrap;
  margin: var(--modem-space-sm) 0;
}

.dial-button {
  font-family: var(--modem-type-face);
  font-size: var(--modem-type-scale-base);
  background: transparent;
  color: var(--modem-green-bright);
  border: 1px solid var(--modem-green-bright);
  padding: var(--modem-space-xs) var(--modem-space-md);
  cursor: pointer;
  text-shadow: 0 0 var(--modem-bloom-radius) currentColor;
}

.dial-button:hover:not(:disabled),
.dial-button:focus-visible {
  background: var(--modem-green-bright);
  color: var(--modem-ground);
  text-shadow: none;
}

.dial-button:disabled {
  color: var(--modem-white-dim);
  border-color: var(--modem-white-dim);
  cursor: not-allowed;
  text-shadow: none;
}

.diagnostic {
  font-size: var(--modem-type-scale-small);
  color: var(--modem-white-bright);
  min-height: 1.4em;
}

.diagnostic--warning {
  color: var(--modem-error);
}

/* ---------------------------------------------------------------------
 * The live endpoint panel (Task 2's real connection, wired up here)
 * ------------------------------------------------------------------- */
.endpoint {
  border: 1px solid var(--modem-green-dim);
  padding: var(--modem-space-sm);
  margin-top: var(--modem-space-sm);
}

.endpoint__row {
  display: flex;
  gap: var(--modem-space-xs);
  flex-wrap: wrap;
  align-items: center;
  margin-bottom: var(--modem-space-sm);
}

.endpoint input[type="text"] {
  font-family: var(--modem-type-face);
  background: var(--modem-ground);
  color: var(--modem-white-bright);
  border: 1px solid var(--modem-white-dim);
  padding: var(--modem-space-xs);
}

.endpoint button {
  font-family: var(--modem-type-face);
  background: transparent;
  color: var(--modem-white-bright);
  border: 1px solid var(--modem-white-dim);
  padding: var(--modem-space-xs) var(--modem-space-sm);
  cursor: pointer;
}

.endpoint button:hover:not(:disabled),
.endpoint button:focus-visible {
  border-color: var(--modem-green-bright);
  color: var(--modem-green-bright);
}

.endpoint button:disabled {
  color: var(--modem-white-dim);
  cursor: not-allowed;
}

.endpoint__status {
  color: var(--modem-green-bright);
}

.endpoint select {
  font-family: var(--modem-type-face);
  background: var(--modem-ground);
  color: var(--modem-white-bright);
  border: 1px solid var(--modem-white-dim);
  padding: var(--modem-space-xs);
}

.endpoint select:hover,
.endpoint select:focus-visible {
  border-color: var(--modem-green-bright);
  color: var(--modem-green-bright);
}

/* ---------------------------------------------------------------------
 * mode-select - the live "one device" / "two devices" choice, offered
 * once the overture has played (the same staged design the microphone
 * prompt already followed - see page.js's own doc on it).
 * ------------------------------------------------------------------- */
.mode-select__controls {
  display: flex;
  gap: var(--modem-space-sm);
  flex-wrap: wrap;
  margin: var(--modem-space-sm) 0;
}

/* ---------------------------------------------------------------------
 * mic-intro - what "Two devices" reveals before it asks for anything:
 * the second-machine requirement and one explicit "Enable microphone"
 * action, so the browser's own permission prompt is not the first thing
 * that happens with no explanation attached to it yet.
 * ------------------------------------------------------------------- */
.mic-intro {
  border: 1px solid var(--modem-green-dim);
  padding: var(--modem-space-sm);
  margin-top: var(--modem-space-sm);
}

/* ---------------------------------------------------------------------
 * wired - the live "one device" split-screen demo: two real Sessions,
 * cross-wired in software exactly as modem-audio's own WiredTransport
 * wires them (see modem-audio/src/transport.rs). Built on `.endpoint`'s
 * own border/row/input/button treatment - this is the same kind of live
 * control surface, just two of them side by side, one green-bright
 * (originate) and one green-dim (answer), the same pairing the
 * generated frames above already draw for the two bands.
 * ------------------------------------------------------------------- */
.wired__panels {
  display: flex;
  gap: var(--modem-space-sm);
  flex-wrap: wrap;
  margin: var(--modem-space-sm) 0;
}

.wired__panel {
  flex: 1 1 320px;
  min-width: 0;
  border: 1px solid var(--modem-green-bright);
  padding: var(--modem-space-sm);
}

.wired__panel--answer {
  border-color: var(--modem-green-dim);
}

.wired__panel-bar {
  color: var(--modem-green-bright);
  font-size: var(--modem-type-scale-small);
  letter-spacing: 0.08em;
  margin-bottom: var(--modem-space-xs);
}

.wired__panel--answer .wired__panel-bar {
  color: var(--modem-green-dim);
}

/* The shared phase line above both panels, and each panel's own status
 * cut down to one word - see page.js's own doc on why the long combined
 * status string now lives once, here, rather than twice, once per panel. */
.wired__phase {
  color: var(--modem-green-bright);
  font-size: var(--modem-type-scale-base);
  margin: 0 0 var(--modem-space-sm) 0;
}

.endpoint__status--short {
  font-size: var(--modem-type-scale-small);
  letter-spacing: 0.05em;
}

/* Compact transcripts for the two side-by-side (or stacked, on a phone -
 * see the mobile layout section below) panels: enough height for a
 * handful of lines, not the full 16em the single unified terminal above
 * gets. Each panel scrolls to its own newest line on append (see
 * appendTerminalLine), so what is visible without scrolling is always
 * the last few lines, and scrolling up reveals the rest - the same
 * "expand by scrolling" shape the generated frames use for width. */
.terminal__output--compact {
  min-height: 4.5em;
  max-height: 8em;
}

.wired__waterfall-wrap {
  margin: var(--modem-space-sm) 0;
}

.wired__waterfall {
  aspect-ratio: 960 / 120;
}

.wired__composer select {
  flex-shrink: 0;
}

.mode-badge {
  color: var(--modem-ground);
  background: var(--modem-green-bright);
  font-size: var(--modem-type-scale-small);
  padding: 0 var(--modem-space-xs);
  letter-spacing: 0.05em;
}

/* ---------------------------------------------------------------------
 * scenarios - the two ways to try modem: one device (split screen, the
 * default) and two devices (the real product). `win`/`bar`/`screen` name
 * the terminal-window chrome the same way modem-tui/examples/mockup.rs
 * names it for its own comparison page - same concept, same names, so
 * it reads as one product in both places.
 *
 * The frames inside `.screen` are generated - see web/_gen/frames.py
 * and the BEGIN/END markers around them in index.html - straight out
 * of App::render_into. Every colour on a cell is inline and literal on
 * purpose: it is the exact colour the real crate drew for that cell,
 * not a page decoration, so no token stands in for it here. The window
 * chrome around it is ordinary page furniture and uses tokens like
 * everything else.
 * ------------------------------------------------------------------- */
.scenario {
  margin-bottom: var(--modem-space-lg);
}

.scenario:last-child {
  margin-bottom: 0;
}

.scenario__label {
  margin-bottom: var(--modem-space-xs);
}

/* width: max-content so the window hugs the fixed-width monospace frame
   it holds, rather than stretching to fill the wider container and
   leaving a slab of empty chrome beside real content - the container is
   now wide enough that the frame renders at its natural size within it
   (max-width: 100% is the fallback once a viewport genuinely cannot fit
   that natural width, e.g. narrow phones, where .screen's own
   overflow-x still applies). */
.win {
  border: 1px solid var(--modem-white-dim);
  background: var(--modem-ground);
  width: max-content;
  max-width: 100%;
  margin: var(--modem-space-sm) 0 0 0;
}

/* The narrower "one device" pair (two 72-column single panes) shown in
 * place of the 100-column split below the mobile breakpoint - see the
 * "Mobile" section at the end of this file. Hidden by default; the two
 * frames are real, different renders (modem-tui/examples/mockup.rs's
 * one-device-mobile-a/b), not the wide one scaled down. */
.win-group--narrow {
  display: none;
}

.bar {
  background: var(--modem-ground);
  border-bottom: 1px solid var(--modem-white-dim);
  color: var(--modem-white-dim);
  font-size: var(--modem-type-scale-small);
  letter-spacing: 0.08em;
  padding: var(--modem-space-xs) var(--modem-space-sm);
}

/* overflow-x: auto already clips cleanly at this container's own edge -
 * .win's max-width: 100% above stops it ever growing past its parent,
 * so a wide frame scrolls inside its own box rather than painting past
 * it. The two background gradients are the classic CSS-only "scroll
 * shadow": one pair scrolls with the content and simply paints over the
 * ground colour at each edge, the other pair is fixed to the viewport
 * and only shows through once the scrolled-with-content one has moved
 * out of the way - so a shadow appears on whichever side still has more
 * to reveal, and both disappear once nothing is left to scroll to. No
 * JavaScript scroll-position tracking needed for what is otherwise a
 * layout-only affordance. overscroll-behavior-x keeps a scroll that
 * reaches either end from chaining into the page itself.
 */
.screen {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  padding: var(--modem-space-sm);
  background:
    linear-gradient(to right, var(--modem-ground) 30%, rgba(10, 10, 10, 0)) left,
    linear-gradient(to left, var(--modem-ground) 30%, rgba(10, 10, 10, 0)) right,
    linear-gradient(to right, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0)) left,
    linear-gradient(to left, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0)) right;
  background-repeat: no-repeat;
  background-color: var(--modem-ground);
  background-size: 2ch 100%, 2ch 100%, 1ch 100%, 1ch 100%;
  background-attachment: local, local, scroll, scroll;
}

/* Fixed-width, real terminal output - reflowing it would break every
   box-drawing alignment, so a narrow viewport scrolls the frame
   sideways instead (same choice mockup.rs's own comparison page makes
   for its `.row`) rather than shrinking it past legibility or letting
   it overflow the page. */
.screen pre {
  margin: 0;
  width: max-content;
  font-family: var(--modem-type-face);
  font-size: clamp(0.6rem, 1.3vw, 1rem);
  line-height: 1.15;
}

/* ---------------------------------------------------------------------
 * phase-row - one handshake phase in the explainer: label, description,
 * play control.
 * ------------------------------------------------------------------- */
.phase-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* One header line (the Play/Stop control beside its own phase name),
 * the description flowing full width beneath it - not a control down
 * one side with every paragraph squeezed into whatever width is left
 * beside it, which is what this looked like before. Same shape at
 * every viewport rather than a separate mobile-only layout: the
 * problem (a narrow text column fighting the button for space) was
 * never really a mobile-only one, just most visible there. */
.phase-row {
  padding: var(--modem-space-sm) 0;
  border-bottom: 1px solid var(--modem-white-dim);
}

.phase-row:last-child {
  border-bottom: none;
}

.phase-row--active {
  /* color-mix, not a hardcoded rgba - this was pinned to amber's own
     literal RGB triplet before the page moved to green, invisible to
     the earlier find-and-replace pass because it never went through
     var(--modem-amber-bright) in the first place. */
  background: color-mix(in srgb, var(--modem-green-bright) 8%, transparent);
}

.phase-row__header {
  display: flex;
  align-items: center;
  gap: var(--modem-space-sm);
}

.phase-row__play {
  font-family: var(--modem-type-face);
  flex-shrink: 0;
  background: transparent;
  color: var(--modem-green-bright);
  border: 1px solid var(--modem-green-bright);
  padding: var(--modem-space-xs) var(--modem-space-sm);
  cursor: pointer;
  min-width: 6ch;
}

.phase-row__play:hover:not(:disabled),
.phase-row__play:focus-visible {
  background: var(--modem-green-bright);
  color: var(--modem-ground);
}

.phase-row__play:disabled {
  color: var(--modem-white-dim);
  border-color: var(--modem-white-dim);
  cursor: not-allowed;
}

.phase-row__label {
  color: var(--modem-green-bright);
  font-size: var(--modem-type-scale-base);
}

.phase-row__description {
  margin: var(--modem-space-xs) 0 0 0;
  color: var(--modem-white-bright);
}

/* ---------------------------------------------------------------------
 * download-row - platform, architecture, link.
 * ------------------------------------------------------------------- */
.download-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.download-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--modem-space-sm);
  padding: var(--modem-space-xs) 0;
  border-bottom: 1px solid var(--modem-white-dim);
}

.download-row:last-child {
  border-bottom: none;
}

.download-row__platform {
  color: var(--modem-white-bright);
}

.download-row__link {
  color: var(--modem-green-bright);
  border: 1px solid var(--modem-green-bright);
  padding: var(--modem-space-xs) var(--modem-space-sm);
  text-decoration: none;
}

.download-row__link:hover,
.download-row__link:focus-visible {
  background: var(--modem-green-bright);
  color: var(--modem-ground);
}

.downloads__note {
  font-size: var(--modem-type-scale-small);
  color: var(--modem-white-dim);
}

/* ---------------------------------------------------------------------
 * endorsement - the DBHQ footer line. Nothing more.
 * ------------------------------------------------------------------- */
.endorsement {
  text-align: center;
  padding: var(--modem-space-md) 0 var(--modem-space-lg) 0;
  font-size: var(--modem-type-scale-small);
  color: var(--modem-white-dim);
}

/* ---------------------------------------------------------------------
 * Mobile - phones and small tablets. The threshold is 48rem (768px):
 * the generated frame's own font-size clamp (.screen pre, above) is
 * already pinned at its 0.6rem floor by about 738px, so below 768px
 * there is nothing left to gain from the wide split frame - a phone
 * gets the narrower real render instead (see .win-group--narrow above
 * and modem-tui/examples/mockup.rs's one-device-mobile-a/b), not the
 * same frame shrunk past legibility.
 * ------------------------------------------------------------------- */
@media (max-width: 48rem) {
  /* Gutters: the page's own --modem-space-md (4ch, roughly 64px on a
     16px root) is a comfortable desktop margin and a poor one on a
     390px screen, where it alone eats a fifth of the width before any
     content starts. --modem-space-sm is an existing token, not a new
     hardcoded value - reusing it here rather than inventing a
     "mobile gutter" token that would only ever apply to one media
     query. */
  main,
  .hero__content {
    padding-left: var(--modem-space-sm);
    padding-right: var(--modem-space-sm);
  }

  /* The one-device frame: real render swap, not a CSS shrink - see the
     banner comment above .win-group--narrow. */
  .win--wide {
    display: none;
  }

  .win-group--narrow {
    display: flex;
    flex-direction: column;
    gap: var(--modem-space-sm);
  }

  /* The live wired demo: stacked, not side by side pressed down to an
     unreadable column width. flex-basis: 320px on .wired__panel (see
     above) is wider than a phone's own content width, so without this
     the two panels would sit on one line, each squeezed far narrower
     than 320px, rather than each getting the full width in turn. */
  .wired__panels {
    flex-direction: column;
  }

  .wired__panel {
    flex-basis: auto;
  }

  /* Touch targets: 44px is the WCAG 2.2 / iOS minimum comfortable
     target. 16px input/select text is not a style choice - below that,
     iOS Safari zooms the whole page in on focus, which this rule
     prevents outright rather than fighting after the fact. */
  .dial-button,
  .endpoint button,
  .endpoint select,
  .phase-row__play,
  .download-row__link {
    min-height: 44px;
  }

  .endpoint input[type="text"],
  .endpoint select {
    min-height: 44px;
    font-size: 16px;
  }
}
