/* dehpeh.dev — design tokens.
   Default is the light theme, which is what stripe.dev serves when the system
   does not ask for dark. It is close to monochrome: one ink colour on a faintly
   tinted off-white. Other themes override the same tokens below. */
:root {
  --red: #7A1F1F;
  --term-bg: #1E1E1E;
  --term-bar: #D7D9DB;
  --term-fg: #E8E8E8;
  --term-dim: #8A8A8A;
  --term-accent: #7FD1C1;
  --bg: #E8E8E8;
  --bg-image: linear-gradient(20deg, #F1F9F3, #F3F1F9, #F9F3F1);
  --panel: rgba(30, 30, 30, 0.06);
  --line: rgba(30, 30, 30, 0.2);
  --dim: rgba(30, 30, 30, 0.66);
  --bright: #1E1E1E;
  --teal: #1E1E1E;
  --purple: #1E1E1E;
  --gold: #1E1E1E;
  --blue: #1E1E1E;
  --scrim: rgba(232, 232, 232, 0.88);
  --sans: "Archivo", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --mono: "IBM Plex Mono", "SF Mono", Menlo, monospace;
}

/* Night Owl. The dark palette stripe.dev uses when the system prefers dark. */
[data-theme="night-owl"] {
  --red: #EF5350;
  --term-bg: #04121F;
  --term-bar: #C0C7D1;
  --term-fg: #C0C7D1;
  --term-dim: #5F7D97;
  --term-accent: #75D1C4;
  --bg: #011627;
  --bg-image: none;
  --panel: #0A2534;
  --line: #14344B;
  --dim: #5F7D97;
  --bright: #C0C7D1;
  --teal: #75D1C4;
  --purple: #9372C9;
  --gold: #F0C078;
  --blue: #6E9CF1;
  --scrim: rgba(1, 22, 39, 0.85);
}

/* Amber CRT. Phosphor on black, for the full terminal fantasy. */
[data-theme="amber"] {
  --red: #FF6B00;
  --term-bg: #0B0700;
  --term-bar: #FFB000;
  --term-fg: #FFB000;
  --term-dim: #8A6A1F;
  --term-accent: #FFC547;
  --bg: #0B0700;
  --bg-image: none;
  --panel: #1A1200;
  --line: #3D2B00;
  --dim: #8A6A1F;
  --bright: #FFB000;
  --teal: #FFC547;
  --purple: #D98A00;
  --gold: #FFB000;
  --blue: #FFC547;
  --scrim: rgba(11, 7, 0, 0.88);
  --sans: "IBM Plex Mono", "SF Mono", Menlo, monospace;
}

/* Paper. High contrast, no tint, for reading long posts. */
[data-theme="paper"] {
  --red: #8B2E1E;
  --term-bg: #141412;
  --term-bar: #DAD8D2;
  --term-fg: #FBFAF7;
  --term-dim: #9A968C;
  --term-accent: #D9A066;
  --bg: #FBFAF7;
  --bg-image: none;
  --panel: rgba(20, 20, 18, 0.05);
  --line: rgba(20, 20, 18, 0.18);
  --dim: rgba(20, 20, 18, 0.6);
  --bright: #141412;
  --teal: #7A4A1E;
  --purple: #7A4A1E;
  --gold: #7A4A1E;
  --blue: #7A4A1E;
  --scrim: rgba(251, 250, 247, 0.9);
}

/* 90s vibes. A nod to the joke theme on stripe.dev. */
[data-theme="90s"] {
  --red: #CC0000;
  --term-bg: #000080;
  --term-bar: #C0C0C0;
  --term-fg: #FFFFFF;
  --term-dim: #A8A8C8;
  --term-accent: #FFFF55;
  --bg: #F5F1FF;
  --bg-image: none;
  --panel: #E6DEFF;
  --line: #9A8FC4;
  --dim: #333333;
  --bright: #000000;
  --teal: #0000EE;
  --purple: #551A8B;
  --gold: #CC0000;
  --blue: #0000EE;
  --scrim: rgba(245, 241, 255, 0.9);
  --sans: "Times New Roman", Times, serif;
  --mono: "Courier New", Courier, monospace;
}

[data-theme="90s"] .prose a,
[data-theme="90s"] .row:hover .row__name { text-decoration: underline; }

* { margin: 0; padding: 0; box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  background-color: var(--bg);
  background-image: var(--bg-image);
  background-attachment: fixed;
  color: var(--dim);
  font-family: var(--sans);
  font-weight: 300;
  letter-spacing: -0.03em;
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; text-decoration: none; }

/* ============ MOTION ============
   Vocabulary borrowed from stripe.dev: an expo-out curve for anything that
   moves, fast linear fades for anything that only changes colour, and a
   step-end blink for the terminal caret. */
:root { --ease: cubic-bezier(0.19, 1, 0.22, 1); }

@keyframes fadein { from { opacity: 0; } to { opacity: 1; } }

@keyframes rise {
  from { opacity: 0; transform: translateY(16px); }
  to { opacity: 1; transform: none; }
}

@keyframes px-in {
  from { opacity: 0; transform: scale(0.3); }
  to { opacity: 1; transform: none; }
}

@keyframes blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }

@keyframes drift {
  from { transform: translateY(0); }
  to { transform: translateY(4px); }
}

/* Scroll reveal. Content only starts hidden when JS is present to reveal it,
   so the page still reads fine with scripts off. */
.js [data-reveal] { opacity: 0; transform: translateY(18px); }

.js [data-reveal].is-in {
  opacity: 1;
  transform: none;
  transition: opacity 0.5s linear, transform 0.65s var(--ease);
  transition-delay: var(--d, 0ms);
}

/* Hero builds itself on load. */
.hero__title { animation: rise 0.85s var(--ease) both; }
.hero__sub { animation: rise 0.85s var(--ease) 0.1s both; }
.hero__stamps { animation: fadein 0.8s ease 0.5s both; }

/* Each module of the wordmark pops in, left to right. */
.pixel-word rect {
  transform-box: fill-box;
  transform-origin: center;
  animation: px-in 0.34s var(--ease) both;
  animation-delay: var(--d, 0ms);
}

.plus { animation: drift 3.5s ease-in-out infinite alternate; animation-delay: var(--d, 0s); }

/* Colour crossfade while a theme swaps, then removed so hovers stay snappy. */
.theming, .theming *, .theming *::before, .theming *::after {
  transition: background-color 0.35s linear, color 0.35s linear,
              border-color 0.35s linear, fill 0.35s linear !important;
}

/* ============ SLOW SPECTRUM ============ */
/* One clock for the whole page. A registered custom property is the only kind
   that can be animated, so the hue is registered and swept on :root; everything
   painted with it (a hovered row, a selection, a module of the wordmark) is at
   the same point in the sweep rather than each starting its own from red.

   Pastel is the whole point. Lightness stays high and saturation stops well
   short of full, so the hue can cross the entire wheel without any part of it
   arriving as neon. --spectrum is registered as a colour too, and it is what
   descendants inherit: registered means it computes to a real colour up here
   instead of inheriting as an unresolved var() that would re-resolve against
   each element's own (initial) hue and come out red everywhere. */
@property --spectrum-hue {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@property --spectrum {
  syntax: "<color>";
  inherits: true;
  initial-value: #F2A9CE;
}

@keyframes spectrum-cycle { to { --spectrum-hue: 360deg; } }

:root {
  --spectrum: hsl(var(--spectrum-hue) 80% 78%);
  /* The sweep never gets dark enough to need light type on top of it, so the
     ink that pairs with it is a constant rather than a per-theme token. */
  --spectrum-ink: #1E1E1E;
  animation: spectrum-cycle 24s linear infinite;
}

/* html.motion-reduced * below does not match html itself, so the sweep has to
   be stopped by name. It parks on a pink rather than wherever the cycle stops.

   This class is the only thing that stops it, deliberately: motionEnabled() in
   site.js treats animation as on by default and consults the system setting
   only when a visitor has asked for "motion auto". Testing the media query here
   as well would override that choice, freezing the sweep for anyone whose OS
   has Reduce Motion on even though this site's own switch says otherwise. */
html.motion-reduced {
  animation: none;
  --spectrum-hue: 320deg;
}

::selection { background: var(--spectrum); color: var(--spectrum-ink); }

/* ============ TOP BAR ============ */
/* No rule under the bar and no fill behind it: the chips sit straight on the
   page and content scrolls beneath them, the way stripe.dev has it. */
.topbar {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  gap: 3px;
  padding: 12px;
  background: transparent;
}

.topbar__flag {
  display: grid;
  place-items: center;
  padding: 5px 8px;
  background: var(--panel);
  border-radius: 2px;
}

.topbar__nav {
  display: flex;
  gap: 3px;
  flex-wrap: wrap;
}

.topbar__nav a,
.topbar__console {
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 300;
  line-height: 1;
  /* Negative tracking is what makes these read dense at this size. */
  letter-spacing: -0.025em;
  text-transform: uppercase;
  color: var(--teal);
  background: var(--panel);
  border: none;
  border-radius: 2px;
  padding: 5px 8px;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}

.topbar__nav a:hover,
.topbar__console:hover {
  background: var(--teal);
  color: var(--bg);
}

.topbar__console { margin-left: auto; }

/* The bracketed key tracks tighter than its label and shares its colour. */
.key { letter-spacing: -0.08em; }

/* ============ SHARED ============ */
.section-label {
  font-family: var(--mono);
  font-weight: 300;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--dim);
  border-bottom: 1px solid var(--line);
  padding-bottom: 0.5rem;
  margin-bottom: 1.25rem;
}

.section-label--pad { margin: 0 1.5rem 1.25rem; }

.chip {
  display: inline-block;
  font-family: var(--mono);
  font-size: 0.75rem;
  text-transform: uppercase;
  color: var(--purple);
  border: 1px dotted var(--purple);
  padding: 2px 5px 2.5px;
}

.arr {
  font-size: 0.7em;
  vertical-align: 0.15em;
  display: inline-block;
  transition: transform 0.3s var(--ease);
}

a:hover .arr, .featured__card:hover .arr { transform: translate(3px, -3px); }

/* stripe.dev outlines its buttons in both themes rather than filling them. */
.btn {
  display: inline-block;
  font-family: var(--mono);
  font-size: 0.85rem;
  color: var(--bright);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 2px;
  padding: 0.55rem 1rem;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}

.btn:hover {
  background: var(--bright);
  color: var(--bg);
  border-color: var(--bright);
}

/* Full-width pill, the shape stripe.dev uses on its Get help cards.
   Doubled class so it outranks the mono-tracking rule further down, which
   lists .btn and would otherwise reset the weight and family. */
.btn.btn--pill {
  display: block;
  width: 100%;
  text-align: center;
  font-family: var(--sans);
  font-size: 0.875rem;
  font-weight: 500;
  letter-spacing: -0.03em;
  border-radius: 99px;
  padding: 10px 12px 12px;
}

.count { color: var(--dim); font-weight: 300; }

/* On a section heading the count rides as a superscript. Scoped, because at
   filter-list size the same treatment renders it about four pixels tall. */
.index__title .count {
  font-size: 0.28em;
  letter-spacing: -0.02em;
  vertical-align: super;
  margin-left: 0.15em;
}

/* Mono runs looser than the sans: Stripe tracks it at -0.012em, not -0.03em. */
.section-label, .chip, .btn, .more-link,
.stat__label, .table__head, .row__date, .row__type, .filter__clear,
.sc__thead, .sc__tab, .sc__links a, .help-card__example,
.footer__link, .footer__legal, .console__bar, .console__bar button,
.console__out, .console__prompt, .console__line input {
  font-weight: 300;
  letter-spacing: -0.012em;
}

button, input { font-weight: 300; }

.plus {
  position: absolute;
  font-family: var(--mono);
  color: var(--dim);
  opacity: 0.5;
  font-size: 0.9rem;
  pointer-events: none;
}

/* ============ HERO ============ */
.hero {
  position: relative;
  padding: 4.5rem 1.5rem 4rem;
  border-bottom: 1px solid var(--line);
}

.hero__title {
  font-size: clamp(3rem, 10.5vw, 9rem);
  font-weight: 300;
  line-height: 0.84;
  letter-spacing: -0.07em;
  color: var(--bright);
  max-width: 12ch;
}

.pixel-word {
  display: inline-block;
  vertical-align: baseline;
  line-height: 0;
  margin-left: 0.12em;
}

.pixel-word svg { display: block; height: 0.72em; width: auto; }

/* Each module answers on its own, so running a cursor across the wordmark
   lights the squares it crosses rather than the whole word at once. */
.pixel-word rect { transition: fill 0.15s linear; }
.pixel-word rect:hover { fill: var(--spectrum); }

.hero__sub {
  margin-top: 2.5rem;
  font-size: clamp(1.3rem, 2.76vw, 2.4rem);
  font-weight: 300;
  line-height: 1.05;
  letter-spacing: -0.045em;
  max-width: 38ch;
}

.hero__stamps {
  position: absolute;
  top: 3.5rem;
  right: 2rem;
  display: flex;
  gap: 0.75rem;
}

.stamp {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  border: 1px solid var(--gold);
  border-radius: 50%;
  color: var(--gold);
  font-family: var(--mono);
  font-size: 0.8rem;
  animation: spin-hold 14s linear infinite;
}

.stamp--globe svg { animation: spin 14s linear infinite; }

@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }

/* ============ FEATURED ============ */
.featured {
  display: grid;
  grid-template-columns: 1fr 1fr;
  border-bottom: 1px solid var(--line);
}

.featured__col {
  padding: 1.5rem;
  display: flex;
  flex-direction: column;
}

.featured__col + .featured__col { border-left: 1px dotted var(--line); }

/* Figure left, words right. The figure is a fixed small plate rather than a
   column-width block: at full width it took about sixty per cent of the
   section's height and put a decorative canvas above the headline in the
   reading order, which is backwards for a card whose whole job is to sell the
   title. Sizing it in vw with a floor keeps it a plate at every width instead
   of growing with the column. */
.featured__body {
  /* The ceiling is what the figure reaches on a desktop column; the floor is
     held well below 1.5x of the old one because under 900px the cards go
     single-column and the text has to keep a readable measure beside it. */
  --fig: clamp(124px, 20vw, 270px);
  display: grid;
  grid-template-columns: var(--fig) minmax(0, 1fr);
  grid-template-rows: auto auto;
  column-gap: clamp(0.9rem, 1.4vw, 1.4rem);
  row-gap: 0.9rem;
  align-items: start;
}

.featured__body .art-mount { grid-column: 1; grid-row: 1 / 3; }

/* The stacked layout needed a bottom margin under the figure; here the grid
   owns that spacing. */
.featured__body .art { margin-bottom: 0; }

.featured__body .featured__card { grid-column: 2; grid-row: 1; }

.featured__card { display: block; flex: 1; }

.featured__card h2 {
  font-size: clamp(1.3rem, 1.9vw, 1.85rem);
  font-weight: 300;
  letter-spacing: -0.04em;
  line-height: 1.2;
  color: var(--bright);
  margin-bottom: 0.7rem;
  text-wrap: balance;
  transition: color 0.15s;
}

.featured__card:hover h2 { color: var(--teal); }

/* Slightly looser than the hero's tracking-tight dek: this one sits in a
   narrower column and runs to three or four lines, where 1.15 leading closes
   up. */
.featured__card p {
  font-size: clamp(0.98rem, 1.05vw, 1.06rem);
  font-weight: 300;
  letter-spacing: -0.02em;
  line-height: 1.38;
  margin-bottom: 0.8rem;
  max-width: 62ch;
  text-wrap: pretty;
}

/* Sits after .btn--pill so these win: the card action is the accent-coloured
   one. It sizes to its label rather than spanning the column, which a pill
   stretched the full width of the old figure-width card used to do. */
.more-link {
  color: var(--teal);
  border-color: var(--teal);
  margin-top: 1.5rem;
}

/* .btn--pill sets width: 100%, and an explicit width wins over justify-self,
   so the pill has to be handed back its intrinsic width here. */
.featured__body .more-link {
  grid-column: 2;
  grid-row: 2;
  display: inline-block;
  width: auto;
  justify-self: start;
  margin-top: 0;
}

.more-link:hover {
  background: var(--teal);
  border-color: var(--teal);
  color: var(--bg);
  text-decoration: none;
}

/* ============ STATS TICKER ============ */
.stats {
  padding: 1.5rem 0 2rem;
  border-bottom: 1px solid var(--line);
  overflow: hidden;
}

.ticker { overflow: hidden; white-space: nowrap; }

/* Wrapping is the static fallback. The rolling and measuring states both
   force a single unwrapped row, so a group is always measured at its natural
   width rather than wrapped to the container. */
.ticker__track {
  display: flex;
  flex-wrap: wrap;
  row-gap: 0.75rem;
}

.ticker__track.is-rolling,
.ticker__track.is-measuring {
  flex-wrap: nowrap;
  width: max-content;
}

/* Rolls only once JS has measured a group and cloned enough to cover the
   viewport. --shift is exactly one group wide, so the loop is seamless at
   any width, and the duration is derived from that width to hold a constant
   speed rather than a constant lap time. */
.ticker__track.is-rolling {
  animation: ticker var(--dur, 40s) linear infinite;
}

.ticker:hover .ticker__track.is-rolling { animation-play-state: paused; }

.ticker__group {
  display: flex;
  flex-wrap: wrap;
  gap: 3.5rem;
  padding-right: 3.5rem;
}

.ticker__track.is-rolling .ticker__group,
.ticker__track.is-measuring .ticker__group { flex-wrap: nowrap; }

@keyframes ticker {
  from { transform: translateX(0); }
  to { transform: translateX(calc(-1 * var(--shift))); }
}

/* Nothing to clip when the row is not rolling, so let it show in full. */
.ticker:has(.ticker__track:not(.is-rolling)) { overflow: visible; }

/* A stat is a mono label with the value in a dotted tag, divided from its
   neighbour by a rule, rather than the oversized numerals I had before. */
.stat {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0 0.85rem;
  border-left: 1px solid var(--line);
  white-space: nowrap;
}

.stat__label {
  font-family: var(--mono);
  font-weight: 300;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: -0.012em;
  color: var(--dim);
}

.stat__value {
  font-family: var(--mono);
  font-weight: 300;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: -0.012em;
  color: var(--purple);
  border: 1px dotted var(--purple);
  border-radius: 3px;
  padding: 2px 5px 2.5px;
}

/* ============ INDEX ============ */
.index { border-bottom: 1px solid var(--line); }

.index__head { padding: 2.5rem 1.5rem 1.5rem; }

.index__title {
  font-size: clamp(2rem, 4.5vw, 3.2rem);
  font-weight: 300;
  letter-spacing: -0.05em;
  line-height: 1;
  color: var(--bright);
}

.index__body {
  display: grid;
  grid-template-columns: 300px 1fr;
  gap: 2rem;
  padding: 0 1.5rem 3rem;
  align-items: start;
}

.filter__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.5rem;
}

.filter__head .section-label { flex: 1; margin-bottom: 0; }

.filter__clear {
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--dim);
  background: none;
  border: 1px dotted var(--dim);
  padding: 2px 6px;
  cursor: pointer;
}

.filter__clear:hover { color: var(--teal); border-color: var(--teal); }

/* Type and Topic sit side by side inside the sidebar, each a collapsible
   group headed by a chevron, a folder mark and its name. */
.filter__groups {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 7rem), 1fr));
  gap: 1.25rem 1.5rem;
  margin-top: 1.5rem;
}

.filter__toggle {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  color: var(--teal);
}

.filter__toggle svg { flex: none; }

.filter__chevron { transition: transform 0.2s var(--ease); }
.filter__toggle[aria-expanded="false"] .filter__chevron { transform: rotate(-90deg); }

.filter__group-name {
  font-size: 0.875rem;
  font-weight: 300;
  letter-spacing: -0.03em;
  color: var(--teal);
}

/* Indented to sit under the folder mark rather than the chevron, so the
   options read as belonging to the group above them. */
.filter__list {
  list-style: none;
  margin-top: 0.4rem;
  padding-left: 1.05rem;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}

.filter__toggle[aria-expanded="false"] + .filter__list { display: none; }

.filter__opt {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font-family: var(--sans);
  font-size: 0.875rem;
  font-weight: 300;
  line-height: 1.6;
  letter-spacing: -0.03em;
  color: var(--dim);
  transition: color 0.12s linear;
}

.filter__opt svg { flex: none; }
.filter__opt:hover { color: var(--bright); }
.filter__opt.active { color: var(--bright); }
.filter__opt .count { color: var(--dim); }

.table__head {
  display: grid;
  grid-template-columns: 118px 1fr 84px 1rem;
  gap: 1rem;
  font-family: var(--mono);
  font-size: 0.8rem;
  text-transform: uppercase;
  color: var(--dim);
  border-bottom: 1px solid var(--line);
  padding-bottom: 0.5rem;
}

.row {
  display: grid;
  grid-template-columns: 118px 1fr 84px 1rem;
  gap: 1rem;
  align-items: baseline;
  padding: 0.8rem 0.6rem;
  margin-inline: -0.6rem;
  border-bottom: 1px dotted var(--line);
  cursor: pointer;
  transition: background 0.12s linear;
}

/* The hovered row takes the spectrum as a solid fill. Everything inside it has
   to switch to the fixed dark ink at the same time: the resting colours are
   theme tokens, and on the dark themes those are light enough to disappear
   into a pastel background. */
.row:hover { background: var(--spectrum); }

.row .row__name { transition: color 0.12s linear, transform 0.3s var(--ease); }
.row:hover .row__name { color: var(--spectrum-ink); transform: translateX(6px); }

.row .row__date { transition: color 0.12s linear; }
.row:hover .row__date { color: var(--spectrum-ink); }

.row:hover .row__bullet,
.row:hover .row__type { color: var(--spectrum-ink); border-color: var(--spectrum-ink); }

.row__date {
  font-family: var(--mono);
  font-weight: 300;
  font-size: 0.8rem;
  letter-spacing: -0.012em;
  color: var(--blue);
}

.row__name {
  font-size: 1.5rem;
  font-weight: 300;
  letter-spacing: -0.04em;
  line-height: 1.2;
  color: var(--bright);
  transition: color 0.15s;
}

.row__date {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.row__bullet {
  width: 7px;
  height: 7px;
  flex: none;
  background: currentColor;
}

.row__type {
  justify-self: start;
  font-family: var(--mono);
  font-weight: 300;
  font-size: 0.75rem;
  letter-spacing: -0.012em;
  text-transform: uppercase;
  color: var(--dim);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 2px 5px 2.5px;
}

.row__plus {
  justify-self: end;
  font-family: var(--mono);
  font-size: 0.85rem;
  color: var(--dim);
  transition: transform 0.3s var(--ease), color 0.12s linear;
}

.row:hover .row__plus { color: var(--spectrum-ink); transform: rotate(90deg); }

/* ============ ABOUT ============ */
.ab { padding: 2.5rem 1.5rem 3rem; border-bottom: 1px solid var(--line); }

.ab__links { display: flex; flex-wrap: wrap; gap: 1.1rem; margin-top: 1.4rem; }
.ab__links a { font-family: var(--mono); font-size: 0.75rem; color: var(--teal); }
.ab__links a:hover { text-decoration: underline; }

/* ---- Statement: editorial, figure alongside ---- */
.ab__statement {
  display: grid;
  grid-template-columns: minmax(0, 320px) minmax(0, 1fr);
  gap: 3rem;
  align-items: start;
}

.ab__lede {
  font-size: clamp(1.5rem, 3vw, 2.3rem);
  font-weight: 200;
  letter-spacing: -0.045em;
  line-height: 1.12;
  color: var(--bright);
  margin-bottom: 1.2rem;
  max-width: 26ch;
}

.ab__para { line-height: 1.35; margin-bottom: 0.9rem; max-width: 62ch; }

/* ---- Readout: the console's voice, as a spec sheet ---- */
.ab__readout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 320px);
  gap: 3rem;
  align-items: start;
}

.ab__rows { font-family: var(--mono); font-size: 0.85rem; }
.ab__cmd { color: var(--dim); margin: 0 0 0.6rem; }
.ab__cmd + .ab__row { margin-top: 0; }
.ab__prompt { color: var(--teal); margin-right: 0.5em; }

/* The leaders are a flexed rule rather than a run of dots, so the value stays
   hard right at any width without the row ever wrapping mid-dot. */
.ab__row { display: flex; align-items: baseline; gap: 0.6rem; padding: 0.32rem 0; }
.ab__key { color: var(--dim); white-space: nowrap; }
.ab__dots { flex: 1; border-bottom: 1px dotted var(--line); transform: translateY(-0.25em); }
.ab__val { color: var(--bright); text-align: right; }
.ab__rows .ab__links { margin-top: 0.4rem; }

@media (max-width: 760px) {
  .ab__statement, .ab__readout { grid-template-columns: 1fr; gap: 1.5rem; }
  .ab__readout .ab__readart { order: -1; }
}

/* ============ HELP CARDS ============ */
.help { padding: 2.5rem 1.5rem 3rem; border-bottom: 1px solid var(--line); }

/* Two up, no card chrome. The section rule and the dotted example line do the
   dividing, so the cards read as columns of a sheet rather than boxes. */
.help__grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 2.75rem 3rem;
  margin-top: 1.5rem;
}

.help-card {
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
  min-width: 0;
}

.help-card__text {
  font-size: clamp(1.2rem, 1.9vw, 1.7rem);
  font-weight: 300;
  letter-spacing: -0.04em;
  line-height: 1.2;
  color: var(--bright);
  flex: 1;
}

.help-card__text strong { font-weight: 500; }

.help-card__example {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--mono);
  font-weight: 300;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: -0.012em;
  color: var(--dim);
  border-bottom: 1px dotted var(--line);
  padding-bottom: 0.9rem;
  min-height: 1.9rem;
}

.help-card__bullet {
  width: 7px;
  height: 7px;
  flex: none;
  background: currentColor;
}

/* Caret sits after whatever has been typed so far. */
.help-card__typed::after {
  content: "_";
  margin-left: 0.1em;
  animation: blink 1.2s step-end infinite;
}

/* ============ PROJECT SHOWCASE ============ */
.sc { padding: 2.5rem 1.5rem 3rem; border-bottom: 1px solid var(--line); }

.sc__head {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 1.5rem;
}

.sc__tabs { display: flex; gap: 0; border: 1px solid var(--line); border-radius: 2px; }

.sc__tab {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  background: none;
  border: none;
  border-right: 1px solid var(--line);
  color: var(--dim);
  cursor: pointer;
  padding: 0.4rem 0.8rem;
  transition: color 0.15s, background 0.15s;
}

.sc__tab:last-child { border-right: none; }
.sc__tab:hover { color: var(--bright); }
.sc__tab.is-on { color: var(--bg); background: var(--bright); }

/* Status reads as a word, not a colour alone, so it still parses in the
   near-monochrome themes where every accent resolves to the same ink. */
.sc__status {
  font-family: var(--mono);
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--dim);
}

.sc__status--live { color: var(--teal); }
.sc__status--building { color: var(--gold); }
.sc__status--archived { opacity: 0.6; }

.sc__meta { display: flex; gap: 0.75rem; align-items: baseline; margin-bottom: 0.5rem; }
.sc__year, .sc__count { font-family: var(--mono); font-size: 0.65rem; color: var(--dim); }
.sc__tags { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-bottom: 0.8rem; }

.sc__links { display: flex; flex-wrap: wrap; gap: 0.9rem; }

.sc__links a {
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--teal);
}

.sc__links a:hover { text-decoration: underline; }

/* ---- Plates ---- */
.sc__plates {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 1.5rem;
}

.sc__plate { display: flex; flex-direction: column; }
.sc__plateart { margin-bottom: 0.9rem; }
.sc__platebody { display: flex; flex-direction: column; flex: 1; }

.sc__name {
  font-size: 1.35rem;
  font-weight: 300;
  letter-spacing: -0.04em;
  color: var(--bright);
  margin-bottom: 0.35rem;
}

.sc__blurb { line-height: 1.25; margin-bottom: 0.8rem; flex: 1; }

/* ---- Index ---- */
/* The head and the rows are separate grid containers, so they can only line up
   if every track resolves the same way in both. That means no `auto` track,
   whose width would follow whatever content each happens to hold, and minmax
   floors of 0 so a long unbroken word cannot push one row's column wider than
   the header's. */
.sc__thead, .sc__row {
  display: grid;
  grid-template-columns: 4.5rem minmax(0, 2.4fr) minmax(0, 1fr) 6.5rem 12rem;
  gap: 1.25rem;
  align-items: start;
}

.sc__thead {
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--dim);
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--line);
  align-items: baseline;
}

.sc__row { padding: 0.9rem 0; border-bottom: 1px dotted var(--line); }
.sc__row:hover { background: var(--panel); }
.sc__rowyear, .sc__rowstack { font-family: var(--mono); font-size: 0.72rem; color: var(--dim); }
.sc__rowname { color: var(--bright); display: flex; flex-direction: column; gap: 0.2rem; }
.sc__rowblurb { font-size: 0.8rem; color: var(--dim); line-height: 1.3; }
.sc__rowlinks { display: flex; gap: 0.7rem; flex-wrap: wrap; }
.sc__rowlinks a { font-family: var(--mono); font-size: 0.7rem; color: var(--teal); white-space: nowrap; }

/* ---- Spotlight ---- */
.sc__spot { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 2.5rem; align-items: start; }
.sc__spotbody { display: flex; flex-direction: column; }

.sc__spotname {
  font-size: clamp(1.8rem, 4vw, 3rem);
  font-weight: 200;
  letter-spacing: -0.05em;
  line-height: 1.05;
  color: var(--bright);
  margin-bottom: 0.8rem;
}

.sc__spotblurb {
  font-size: 1.1rem;
  font-weight: 300;
  line-height: 1.3;
  margin-bottom: 1.2rem;
  max-width: 48ch;
}

.sc__steps { display: flex; gap: 0.5rem; margin-top: 1.5rem; }

@media (max-width: 760px) {
  .sc__spot { grid-template-columns: 1fr; gap: 1.2rem; }
  .sc__thead { display: none; }
  .sc__row { grid-template-columns: 1fr; gap: 0.35rem; }
  .sc__head { flex-direction: column; align-items: flex-start; }
}

/* ============ FOOTER ============ */
.footer { padding: 2.5rem 1.5rem 1.5rem; }

.footer__cols {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 2rem;
}

.footer__blurb { margin-bottom: 1rem; max-width: 40ch; }

.footer__link {
  display: block;
  font-family: var(--mono);
  font-size: 0.85rem;
  color: var(--teal);
  padding: 0.2rem 0;
}

.footer__link:hover { text-decoration: underline; }

.footer__legal {
  display: flex;
  justify-content: space-between;
  font-family: var(--mono);
  font-size: 0.75rem;
  text-transform: uppercase;
  margin-top: 3rem;
  padding-top: 1rem;
  border-top: 1px solid var(--line);
}

.footer__legal a { margin-left: 1rem; }
.footer__legal a:hover { color: var(--teal); }

/* ============ ARTICLE ============ */
.article { border-bottom: 1px solid var(--line); }

.article__body {
  max-width: 46rem;
  margin: 0 auto;
  padding: 3rem 1.5rem 4rem;
}

.article__back {
  font-family: var(--mono);
  font-size: 0.8rem;
  letter-spacing: -0.012em;
  color: var(--teal);
}

.article__back:hover { text-decoration: underline; }

.article__meta {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin-top: 2rem;
}

.article__read {
  font-family: var(--mono);
  font-size: 0.8rem;
  letter-spacing: -0.012em;
  color: var(--dim);
}

.article__title {
  font-size: clamp(2.2rem, 5.5vw, 3.6rem);
  font-weight: 300;
  letter-spacing: -0.05em;
  line-height: 1.02;
  color: var(--bright);
  margin-top: 1.25rem;
}

.article__dek {
  font-size: clamp(1.15rem, 2vw, 1.5rem);
  font-weight: 300;
  letter-spacing: -0.03em;
  line-height: 1.25;
  margin-top: 1.25rem;
}

.article__rule {
  border-top: 1px solid var(--line);
  margin: 2.5rem 0;
}

.article__notfound { max-width: 46rem; margin: 0 auto; padding: 5rem 1.5rem; }
.article__notfound p { margin: 1rem 0 2rem; }

/* ---- Prose ---- */
.prose { font-size: 1.14rem; line-height: 1.6; letter-spacing: -0.02em; }

.prose p { margin-bottom: 1.5rem; }

.prose h2 {
  font-size: 1.6rem;
  font-weight: 300;
  letter-spacing: -0.04em;
  line-height: 1.15;
  color: var(--bright);
  margin: 2.75rem 0 1rem;
}

.prose ul { margin: 0 0 1.5rem 1.1rem; }
.prose li { margin-bottom: 0.6rem; }
.prose li::marker { color: var(--teal); }

.prose blockquote {
  border-left: 2px solid var(--gold);
  padding-left: 1.25rem;
  margin: 2rem 0;
  font-size: 1.35rem;
  line-height: 1.3;
  letter-spacing: -0.03em;
  color: var(--bright);
}

.prose pre {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 1rem 1.1rem;
  margin-bottom: 1.5rem;
  overflow-x: auto;
}

.prose code {
  font-family: var(--mono);
  font-weight: 300;
  font-size: 0.86rem;
  letter-spacing: -0.012em;
  line-height: 1.55;
  color: var(--gold);
  white-space: pre;
}

/* ---- Prev / next ---- */
.postnav {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1px;
  background: var(--line);
  border-top: 1px solid var(--line);
}

.postnav__item {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  background: var(--bg);
  padding: 1.75rem 1.5rem;
}

.postnav__item--next { text-align: right; }

.postnav__dir {
  font-family: var(--mono);
  font-size: 0.8rem;
  letter-spacing: -0.012em;
  color: var(--dim);
}

.postnav__title {
  font-size: 1.25rem;
  font-weight: 300;
  letter-spacing: -0.04em;
  line-height: 1.2;
  color: var(--bright);
  transition: color 0.15s;
}

a.postnav__item:hover .postnav__title { color: var(--teal); }

.row-empty { padding: 2rem 0; }

/* ============ CONSOLE ============ */
/* Docked to the right rather than centred, and deliberately without a scrim:
   the page stays readable and clickable while the console is open, so it reads
   as a tool alongside the site instead of a modal on top of it. */
.console[hidden] { display: none; }

/* The layer is the whole viewport and takes no pointer itself; the window is
   placed inside it absolutely rather than by flex alignment, because a dragged
   window has to be addressed in left/top and a flex child cannot be. */
.console {
  position: fixed;
  inset: 0;
  z-index: 100;
  pointer-events: none; /* only the window itself takes the pointer */
}

/* Overflow is hidden so the grips below sit inside the frame rather than
   spilling past it. The output pane keeps its own scrolling, so no content is
   lost to it. */
.console__window {
  pointer-events: auto;
  position: absolute;
  top: 1.1rem;
  right: 1.1rem;
  width: min(600px, calc(100% - 2.2rem));
  height: calc(100% - 2.2rem);
  min-width: 260px;
  min-height: 140px;
  /* No `resize: both`: the browser's own grip only offers the bottom-right
     corner, and the grips below give all four edges as well. */
  overflow: hidden;
  display: flex;
  flex-direction: column;
  background: var(--term-bg);
  border: 1px solid var(--line);
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.28);
}

/* Once dragged the window is positioned from the left, so the right anchor it
   started with has to stop competing. */
.console__window.is-placed { right: auto; }

/* Eight invisible strips inside the frame, injected by site.js. They sit inside
   rather than straddling the border because the window clips its overflow, and
   the corners outrank the edges so a corner drag is never read as an edge one. */
.console__grip { position: absolute; z-index: 3; }
.console__grip--n { top: 0; left: 0; right: 0; height: 6px; cursor: ns-resize; }
.console__grip--s { bottom: 0; left: 0; right: 0; height: 6px; cursor: ns-resize; }
.console__grip--e { top: 0; right: 0; bottom: 0; width: 6px; cursor: ew-resize; }
.console__grip--w { top: 0; left: 0; bottom: 0; width: 6px; cursor: ew-resize; }

.console__grip--ne,
.console__grip--nw,
.console__grip--se,
.console__grip--sw { width: 14px; height: 14px; z-index: 4; }

.console__grip--ne { top: 0; right: 0; cursor: nesw-resize; }
.console__grip--nw { top: 0; left: 0; cursor: nwse-resize; }
.console__grip--se { bottom: 0; right: 0; cursor: nwse-resize; }
.console__grip--sw { bottom: 0; left: 0; cursor: nesw-resize; }

/* The window buttons sit in the bar, which is the drag surface, so they take
   the pointer back and show their own cursor. */
.console__glyphs button {
  display: block;
  padding: 0;
  line-height: 0;
  cursor: pointer;
}

.console__bar {
  flex: none;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 0.5rem;
  padding: 0.3rem 0.5rem;
  background: var(--term-bar);
  color: #1E1E1E;
  cursor: move;
  user-select: none;
  -webkit-user-select: none;
}

.console.is-dragging, .console.is-dragging .console__bar { cursor: grabbing; }

.console__glyphs { display: flex; gap: 5px; }
.console__glyphs svg { display: block; }

.console__title {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.16em;
  text-align: center;
}

.console__bar button {
  justify-self: end;
  font-family: var(--mono);
  font-size: 0.85rem;
  line-height: 1;
  background: none;
  border: none;
  color: inherit;
  cursor: pointer;
  padding: 2px 4px;
}

.console__bar button:hover { opacity: 0.6; }

.console__out {
  flex: 1;
  /* the banner sizes itself against this pane's width, not the viewport's */
  container-type: inline-size;
  font-family: var(--mono);
  font-size: 0.8rem;
  line-height: 1.55;
  color: var(--term-fg);
  padding: 0.75rem 0.85rem 0;
  overflow-y: auto;
  white-space: pre-wrap;
  word-break: break-word;
}

.console__out .c-dim { color: var(--term-dim); }
.console__out .c-teal { color: var(--term-accent); }
.console__out .c-gold { color: var(--term-accent); }
.console__out .c-err { color: var(--red); }
.console__out .c-line { display: block; animation: fadein 0.18s ease both; }

/* The banner is set in the oblique figlet face called "slant"; site.js carries
   the glyphs.

   pre, not the pre-wrap the output pane sets: it is drawn on a character grid
   and a wrapped row would land under the wrong column.

   Since the window became resizable there is no single size that fits, so the
   banner is measured against its container rather than the root: 62 columns of
   a 0.6em-advance mono need about 2.3cqw each to sit inside the pane, and the
   min() keeps 0.62rem as the ceiling so a wide window does not inflate it. */
.console__banner {
  color: var(--term-dim);
  font-size: min(0.62rem, 2.3cqw);
  line-height: 1.02;
  display: block;
  margin-bottom: 0.35rem;
  white-space: pre;
}

.console__rule { color: var(--term-dim); opacity: 0.55; display: block; overflow: hidden; height: 1.1em; }

.console__line {
  flex: none;
  position: relative;
  display: flex;
  align-items: baseline;
  gap: 0.5em;
  padding: 0.35rem 0.85rem 0.85rem;
  font-family: var(--mono);
  font-size: 0.8rem;
  cursor: text;
}

.console__prompt { color: var(--term-accent); }

/* The real input sits transparently on top of a mirror of its own text. That
   is what allows a solid block cursor: the caret is drawn as an element
   between the two halves of the mirrored string, so it lands in the right
   place even when editing mid-line. */
.console__mirror {
  flex: 1;
  min-width: 0;
  color: var(--term-fg);
  white-space: pre;
  overflow: hidden;
}

.console__cursor {
  display: inline-block;
  width: 0.62em;
  height: 1.05em;
  vertical-align: text-bottom;
  background: var(--term-accent);
  animation: blink 1.1s step-end infinite;
}

.console:not(.is-focused) .console__cursor { opacity: 0.45; animation: none; }

.console__line input {
  position: absolute;
  left: 0.85rem;
  right: 0.85rem;
  top: 0.35rem;
  width: auto;
  font: inherit;
  color: transparent;
  caret-color: transparent;
  background: none;
  border: none;
  outline: none;
  padding: 0 0 0 1em;
}

.console__line input::selection { background: var(--term-accent); color: var(--term-bg); }

@media (max-width: 700px) {
  .console__window {
    top: 0.6rem;
    right: 0.6rem;
    width: calc(100% - 1.2rem);
    height: calc(100% - 1.2rem);
  }
}

/* ============ COMPOSER ============ */
/* Opened by an unlisted console command. It writes an entry for posts.js and
   nothing else: there is no network call here, so publishing still means a
   commit. See the header of compose.js. */
.cmp[hidden] { display: none; }

/* Set on <html> while the composer is open. The padding replaces the scrollbar
   the hidden overflow removes, so the page behind does not jump sideways. */
.is-composing {
  overflow: hidden;
  padding-right: var(--scrollbar-width, 0);
}

.cmp {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: var(--scrim);
  display: grid;
  place-items: start center;
  padding: 1.25rem;
  overflow-y: auto;
}

/* min-width is 0 rather than the grid default of auto because the generated
   file in .cmp__out is a <pre> that does not wrap. Its min-content width is
   whatever the longest line happens to be, and as a grid item this window
   cannot shrink below that, which widens the column and pushes the centred
   window off the right of the screen. Zero lets the column stay viewport
   sized and leaves the <pre> to scroll inside its own box, as intended. */
.cmp__window {
  width: min(780px, 100%);
  min-width: 0;
  background: var(--bg);
  border: 1px solid var(--line);
}

.cmp__bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.4rem 0.6rem;
  background: var(--term-bar);
  color: #1E1E1E;
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.16em;
}

.cmp__bar button {
  background: none;
  border: none;
  color: inherit;
  cursor: pointer;
  font-size: 0.85rem;
  line-height: 1;
}

.cmp__body { padding: 1rem; }

.cmp__note {
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--dim);
  margin-bottom: 0.9rem;
}

.cmp__note code { color: var(--teal); }

.cmp__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 0.6rem;
  margin-bottom: 0.6rem;
}

.cmp label,
.cmp__wide {
  display: block;
  font-family: var(--mono);
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--dim);
}

.cmp input,
.cmp select,
.cmp textarea {
  display: block;
  width: 100%;
  margin-top: 0.2rem;
  font-family: var(--mono);
  font-size: 0.8rem;
  color: var(--bright);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 2px;
  padding: 0.4rem 0.5rem;
  text-transform: none;
  letter-spacing: 0;
}

.cmp textarea { resize: vertical; line-height: 1.45; }
.cmp input:focus, .cmp select:focus, .cmp textarea:focus { outline: 1px solid var(--teal); }

.cmp__blockshead,
.cmp__outhead {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: 1.1rem 0 0.5rem;
  font-family: var(--mono);
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--dim);
}

.cmp__status { text-transform: none; letter-spacing: 0; color: var(--teal); }

.cmp__blocks { list-style: none; margin: 0; padding: 0; }

.cmp__block {
  border: 1px solid var(--line);
  border-radius: 2px;
  padding: 0.4rem;
  margin-bottom: 0.5rem;
}

.cmp__blockbar { display: flex; align-items: center; gap: 0.35rem; margin-bottom: 0.35rem; }
.cmp__blockbar select { width: auto; margin: 0; }
.cmp__spacer { flex: 1; }

.cmp__blockbar button {
  font-family: var(--mono);
  font-size: 0.75rem;
  line-height: 1;
  background: none;
  border: 1px solid var(--line);
  border-radius: 2px;
  color: var(--dim);
  cursor: pointer;
  padding: 3px 6px;
}

.cmp__blockbar button:hover { color: var(--bright); border-color: var(--bright); }

.cmp__out {
  font-family: var(--mono);
  font-size: 0.72rem;
  line-height: 1.5;
  color: var(--term-fg);
  background: var(--term-bg);
  border: 1px solid var(--line);
  padding: 0.6rem;
  max-height: 240px;
  overflow: auto;
  white-space: pre;
}

.cmp__tabs {
  display: flex;
  gap: 0;
  border-bottom: 1px solid var(--line);
}

.cmp__tab {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  color: var(--dim);
  cursor: pointer;
  padding: 0.55rem 0.9rem;
}

.cmp__tab:hover { color: var(--bright); }
.cmp__tab.is-on { color: var(--bright); border-bottom-color: var(--teal); }

.cmp__note--warn { color: var(--red); }

.cmp__list { list-style: none; margin: 0 0 0.8rem; padding: 0; }

/* A table without a table: the date column is fixed so the titles line up,
   and the actions sit hard right whatever the title length. */
.cmp__row {
  display: grid;
  grid-template-columns: 6.5em 1fr auto auto;
  gap: 0.6rem;
  align-items: center;
  padding: 0.4rem 0.2rem;
  border-bottom: 1px dotted var(--line);
  font-size: 0.82rem;
}

.cmp__row.is-editing { background: var(--panel); }

.cmp__rowdate { font-family: var(--mono); font-size: 0.72rem; color: var(--dim); }
.cmp__rowtitle { color: var(--bright); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.cmp__rowtype {
  font-family: var(--mono);
  font-size: 0.65rem;
  text-transform: uppercase;
  color: var(--dim);
}

.cmp__rowacts { display: flex; gap: 0.25rem; }

.cmp__rowacts button {
  font-family: var(--mono);
  font-size: 0.7rem;
  line-height: 1;
  background: none;
  border: 1px solid var(--line);
  border-radius: 2px;
  color: var(--dim);
  cursor: pointer;
  padding: 3px 6px;
}

.cmp__rowacts button:hover { color: var(--bright); border-color: var(--bright); }
.cmp__rowacts button[data-mact="del"]:hover { color: var(--red); border-color: var(--red); }

.cmp__empty {
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--red);
  padding: 0.8rem 0.2rem;
}

.cmp__actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.8rem; }
.cmp__actions .btn:disabled { opacity: 0.4; cursor: not-allowed; }

/* ============ FIGURE ART ============ */
/* Window chrome around a canvas figure. The frame is deliberately plain: the
   rules either side of the caption only appear on hover, so a resting card
   shows the label alone and the drag affordance arrives with the cursor. */
.art {
  --art-chrome: 20px;
  aspect-ratio: 302 / 252;
  border: 1px solid var(--line);
  background: var(--panel);
  display: flex;
  flex-direction: column;
  margin-bottom: 1.2rem;
  user-select: none;
  -webkit-user-select: none;
}

/* overflow is hidden because the caption and its flanking rules can measure
   wider than the frame once the figure is used at plate size in the featured
   cards; the rules are decorative, so trimming them beats letting the bar push
   out of the border. */
.art__chrome {
  height: var(--art-chrome);
  flex: none;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 6px;
  border-bottom: 1px solid var(--line);
  overflow: hidden;
}

.art__rule {
  flex: 1;
  height: 1px;
  background: var(--line);
  opacity: 0;
  transition: opacity 0.15s;
}

.art:hover .art__rule { opacity: 1; }

.art__fig {
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
  white-space: nowrap;
}

.art__open {
  flex: none;
  font-size: 0.7rem;
  line-height: 1;
  color: var(--dim);
  opacity: 0;
  transition: opacity 0.15s, color 0.15s;
}

.art:hover .art__open { opacity: 1; }
.art__open:hover { color: var(--teal); }

/* The dot grid sits under the canvas rather than in it, so the renderer never
   has to redraw the backdrop and it stays crisp at any DPR. */
.art__stage {
  flex: 1;
  min-height: 0;
  position: relative;
  margin: 4px;
  border: 1px solid var(--line);
  border-radius: 2px;
  cursor: grab;
  background-image: radial-gradient(var(--line) 1px, transparent 1px);
  background-size: 12px 12px;
  background-position: center;
  overflow: hidden;
}

.art.is-dragging .art__stage { cursor: grabbing; }

/* The renderer reads this colour off the canvas to pick its ink, which is how
   the figure follows the theme without knowing any of the palettes. */
.art__canvas {
  display: block;
  width: 100%;
  height: 100%;
  color: var(--bright);
  touch-action: pan-y;
}

/* Holding a figure runs its clock ten times faster. Nothing else on screen
   says so, which is what the badge is for: it appears on press and pulses for
   as long as the pointer is down. Under motion-reduced the pulse is flattened
   by the global rule at the end of this file, and the badge just sits there. */
.art__tenx {
  position: absolute;
  top: 6px;
  left: 8px;
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.08em;
  color: var(--dim);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s;
}

.art.is-boosting .art__tenx {
  opacity: 1;
  animation: artPulse 1s ease infinite;
}

@keyframes artPulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.25; }
}

.art.is-boosting .art__stage { cursor: grabbing; }

/* The feed figure sits in a column far wider than a card, so it would
   need a ceiling of their own. */
.filter__art { margin-top: 1.5rem; }

/* ============ INSTRUMENTATION PAGE ============ */
.lab__bar {
  display: flex;
  align-items: baseline;
  gap: 1.2rem;
  padding: 1rem 1.5rem;
  border-bottom: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 0.8rem;
}

.lab__back { color: var(--teal); }
.lab__back:hover { text-decoration: underline; }
.lab__title { color: var(--dim); letter-spacing: 0.08em; }

.lab {
  display: grid;
  grid-template-columns: 240px 1fr;
  min-height: calc(100vh - 3.2rem);
}

.lab__panel {
  padding: 1.5rem;
  border-right: 1px dotted var(--line);
}

.lab__params {
  list-style: none;
  margin: 0.8rem 0 1.2rem;
  padding: 0;
}

/* Rows are a two-column grid rather than a flex row with space-between, so
   every value starts at the same x and the column reads as a table. */
.lab__param {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.5rem;
  align-items: baseline;
  padding: 0.18rem 0.35rem;
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--dim);
  cursor: pointer;
  border-left: 2px solid transparent;
}

.lab__param:hover { color: var(--bright); }

.lab__param.is-armed {
  color: var(--bright);
  background: var(--panel);
  border-left-color: var(--teal);
}

.lab__value { font-variant-numeric: tabular-nums; }
.lab__param.is-armed .lab__value { color: var(--teal); }

.lab__reset {
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--dim);
  padding-top: 0.6rem;
  border-top: 1px dotted var(--line);
}

.lab__stage {
  padding: 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  min-width: 0;
}

/* The card figures are locked to a card-shaped box; here the figure is the
   point of the page, so it takes whatever room is left. */
.art--lab {
  aspect-ratio: auto;
  flex: 1;
  min-height: 320px;
  margin-bottom: 0;
}

.lab__hint {
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--dim);
  margin: 0;
}

.lab__actions { display: flex; gap: 0.6rem; flex-wrap: wrap; }

/* ============ RESPONSIVE ============ */
@media (max-width: 900px) {
  .featured { grid-template-columns: 1fr; }
  .featured__col + .featured__col { border-left: none; border-top: 1px dotted var(--line); }
  .index__body { grid-template-columns: 1fr; }
  .help__grid { grid-template-columns: 1fr 1fr; }
  .footer__cols { grid-template-columns: 1fr; }
  .hero__stamps { top: 1.5rem; right: 1rem; }
}

@media (max-width: 560px) {
  .help__grid { grid-template-columns: 1fr; }
  .table__head, .row { grid-template-columns: 92px 1fr 66px; gap: 0.6rem; }
  /* No room for the trailing plus at this width. */
  .table__head span:nth-child(4), .row__plus { display: none; }
  .topbar__nav { overflow-x: auto; flex-wrap: nowrap; }
}

/* Animation is on by default. JS adds .motion-reduced when a visitor turns it
   off via the console, or picks "auto" and their system asks for less motion.
   The ticker gates itself on .is-rolling instead. */
html.motion-reduced *,
html.motion-reduced *::before,
html.motion-reduced *::after {
  animation-duration: 0.01ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.01ms !important;
}

html.motion-reduced { scroll-behavior: auto; }

html.motion-reduced .stamp--globe svg,
html.motion-reduced .plus,
html.motion-reduced .console__prompt::after { animation: none !important; }

html.motion-reduced.js [data-reveal] { opacity: 1; transform: none; }

@media (max-width: 800px) {
  .lab { grid-template-columns: 1fr; }
  .lab__panel { border-right: none; border-bottom: 1px dotted var(--line); }
}

/* Publishing is the one control here that changes what other people see, so it
   is the one that looks different from the rest. */
.cmp__publish {
  color: var(--teal);
  border-color: var(--teal);
}

.cmp__publish:hover { background: var(--teal); color: var(--bg); }
.cmp__signin { color: var(--dim); text-decoration: none; }
