/* ==========================================================================
   Tutorial
   Everything here is additive to app.css + guide.css. The tutorial reuses
   the handbook's .guide / .toc / .prose skeleton and adds the pieces that
   only this page needs: the parts list, the OS tabs, the two interactive
   planners, the cost calculator and the diagrams.
   ========================================================================== */

/* --- Hero extras --------------------------------------------------------- */

.price-tag {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  font-family: var(--font-mono);
  margin: 22px 0 4px;
}

.price-tag .amount {
  font-size: clamp(2.2rem, 7vw, 3.6rem);
  color: var(--green);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 32px rgba(0, 255, 156, .35);
}

.price-tag .per { font-size: .9rem; color: var(--text-3); }

/* The little "you are looking at one right now" strip under the hero. */
.proof {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 20px;
  padding: 12px 16px;
  border: 1px solid var(--line);
  border-left: 2px solid var(--green);
  border-radius: var(--r-sm);
  background: rgba(0, 255, 156, .04);
  font-size: .84rem;
  color: var(--text-2);
  max-width: 640px;
}

.proof i, .proof svg { width: 16px; height: 16px; color: var(--green); flex: none; }
.proof b { color: var(--green-2); font-weight: 500; font-family: var(--font-mono); }

/* --- Part dividers ------------------------------------------------------- */
/* The tutorial is long. Six labelled parts give it a shape you can hold in
   your head, and the reader always knows how far in they are. */

.part {
  display: flex;
  align-items: center;
  gap: 16px;
  margin: 8px 0 34px;
  padding-top: 8px;
  scroll-margin-top: calc(var(--nav-h) + 20px);
}

.part .n {
  font-family: var(--font-mono);
  font-size: 2.1rem;
  line-height: 1;
  color: transparent;
  -webkit-text-stroke: 1px var(--green-4);
  flex: none;
}

.part h2 {
  margin: 0;
  font-size: 1.1rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text);
}

.part p { margin: 3px 0 0; font-size: .82rem; color: var(--text-3); }
.part .line { flex: 1; height: 1px; background: linear-gradient(90deg, var(--line-hot), transparent); }

/* One character per part, at the far end of the rule. The images are plain
   rectangles with the near-black background the model painted behind them —
   `mix-blend-mode: screen` is what lets them sit on the page with no visible
   edge, because screening near-black against near-black yields near-black.
   That is cheaper and far more robust than keying out a background and
   shipping alpha, which the glow around the linework would have made ugly.

   Explicit width and height on the tag as well as here: the row must not
   reflow when the image finally loads. They shrink on tablets and go away
   entirely on phones, where the row has no width to spare — the part heading
   is the content and the creature is a decoration. */
.part .mascot {
  flex: none;
  width: 84px;
  height: 84px;
  mix-blend-mode: screen;
  opacity: .92;
}
@media (max-width: 900px) { .part .mascot { width: 60px; height: 60px; } }
@media (max-width: 640px) { .part .mascot { display: none; } }

/* --- Parts list (what you actually buy) ---------------------------------- */

.parts { display: grid; gap: 14px; margin: 0 0 24px; }

.part-card {
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr) auto;
  gap: 16px;
  align-items: center;
  padding: 16px 18px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--surface);
  transition: border-color .2s, transform .2s;
}

.part-card:hover { border-color: var(--line-hot); transform: translateY(-1px); }

.part-card .ico {
  width: 40px; height: 40px;
  display: grid; place-items: center;
  border-radius: var(--r-sm);
  background: var(--green-dim);
  color: var(--green);
}
.part-card .ico i, .part-card .ico svg { width: 19px; height: 19px; }

.part-card h4 { margin: 0 0 3px; font-size: .95rem; color: var(--text); font-family: var(--font-display); font-weight: 600; letter-spacing: 0; }
.part-card p  { margin: 0; font-size: .8rem; color: var(--text-3); line-height: 1.5; }

.part-card .cost {
  text-align: right;
  font-family: var(--font-mono);
  color: var(--green);
  font-size: 1.05rem;
  white-space: nowrap;
}
.part-card .cost small { display: block; color: var(--text-3); font-size: .64rem; letter-spacing: .08em; text-transform: uppercase; margin-top: 3px; }

@media (max-width: 560px) {
  .part-card { grid-template-columns: 34px minmax(0, 1fr); }
  .part-card .cost { grid-column: 2; text-align: left; }
}

/* --- OS tabs ------------------------------------------------------------- */
/* Most readers are on Windows 11 and a few are on macOS. Rather than writing
   every command twice inline, each block that differs gets a tab strip. The
   choice is remembered across the whole page and across visits. */

.os {
  border: 1px solid var(--line);
  border-radius: var(--r);
  overflow: hidden;
  margin: 0 0 22px;
  background: var(--bg-1);
}

.os-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--line); background: rgba(0,255,156,.03); }

.os-tabs button {
  appearance: none;
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  color: var(--text-3);
  font-family: var(--font-mono);
  font-size: .72rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  padding: 10px 16px;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 7px;
  transition: color .16s, border-color .16s, background .16s;
}

.os-tabs button i, .os-tabs button svg { width: 14px; height: 14px; }
.os-tabs button:hover { color: var(--text-2); background: rgba(0,255,156,.04); }
.os-tabs button[aria-selected="true"] { color: var(--green); border-bottom-color: var(--green); background: rgba(0,255,156,.06); }

.os-panel { padding: 4px 14px 2px; }
.os-panel > pre { margin-bottom: 14px; background: transparent; border: 0; padding: 12px 0; }
.os-panel > p:first-child { margin-top: 12px; }

/* --- Interactive planners: shared shell ---------------------------------- */

.tool {
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  background:
    radial-gradient(120% 100% at 0% 0%, rgba(0,255,156,.06), transparent 60%),
    var(--surface-2);
  padding: 22px;
  margin: 0 0 26px;
  box-shadow: var(--shadow);
}

.tool-head { display: flex; align-items: flex-start; gap: 13px; margin-bottom: 16px; }
.tool-head .ico { width: 34px; height: 34px; display: grid; place-items: center; border-radius: var(--r-sm); background: var(--green-dim); color: var(--green); flex: none; }
.tool-head .ico i, .tool-head .ico svg { width: 17px; height: 17px; }
.tool-head h4 { margin: 0 0 3px; font-size: 1rem; color: var(--text); font-family: var(--font-display); letter-spacing: 0; }
.tool-head p  { margin: 0; font-size: .82rem; color: var(--text-3); line-height: 1.5; }

/* --- Stack chooser modal ------------------------------------------------- */

.modal[hidden] { display: none !important; }

.modal {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  padding: 20px;
  background: rgba(2, 5, 4, .78);
  backdrop-filter: blur(6px);
  animation: modal-in .18s ease-out;
}

@keyframes modal-in { from { opacity: 0 } to { opacity: 1 } }

.modal-box {
  width: min(660px, 100%);
  max-height: min(86dvh, 820px);
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  background: var(--bg-1);
  box-shadow: 0 40px 120px -30px #000, var(--glow);
  overflow: hidden;
  animation: box-in .22s cubic-bezier(.2, .8, .3, 1);
}

@keyframes box-in { from { transform: translateY(14px) scale(.985); opacity: 0 } to { transform: none; opacity: 1 } }

@media (prefers-reduced-motion: reduce) {
  .modal, .modal-box { animation: none; }
}

.modal-top {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--line);
  flex: none;
}

.modal-top h3 { margin: 0; font-size: .95rem; color: var(--text); flex: 1; letter-spacing: 0; }
.modal-top .step-n { font-family: var(--font-mono); font-size: .7rem; color: var(--text-3); letter-spacing: .1em; }

.modal-x {
  appearance: none; background: none; border: 1px solid var(--line);
  color: var(--text-3); width: 28px; height: 28px; border-radius: var(--r-sm);
  cursor: pointer; display: grid; place-items: center; flex: none;
}
.modal-x:hover { color: var(--red); border-color: rgba(255,77,109,.4); }
.modal-x i, .modal-x svg { width: 14px; height: 14px; }

/* Progress bar across the top of the wizard. */
.modal-bar { height: 2px; background: var(--line); flex: none; }
.modal-bar > span { display: block; height: 100%; background: var(--green); box-shadow: var(--glow); transition: width .3s cubic-bezier(.3,.8,.3,1); }

.modal-body { padding: 22px 20px; overflow-y: auto; flex: 1; }
.modal-body > h4 { margin: 0 0 4px; font-size: 1.08rem; color: var(--text); font-family: var(--font-display); letter-spacing: 0; }
.modal-body > .q-sub { margin: 0 0 18px; font-size: .84rem; color: var(--text-3); line-height: 1.55; }

.opts { display: grid; gap: 9px; }

.opt {
  appearance: none;
  text-align: left;
  display: grid;
  grid-template-columns: 30px minmax(0, 1fr);
  gap: 13px;
  align-items: start;
  padding: 13px 15px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--surface);
  color: inherit;
  font: inherit;
  cursor: pointer;
  transition: border-color .16s, background .16s, transform .16s;
}

.opt:hover { border-color: var(--line-hot); background: rgba(0,255,156,.05); transform: translateX(2px); }
.opt:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.opt[aria-pressed="true"] { border-color: var(--green); background: var(--green-dim); }

.opt .k {
  width: 30px; height: 30px; display: grid; place-items: center;
  border-radius: var(--r-sm); background: var(--bg-2); color: var(--green-3);
  border: 1px solid var(--line);
}
.opt .k i, .opt .k svg { width: 15px; height: 15px; }
.opt b { display: block; color: var(--text); font-size: .9rem; font-weight: 600; margin-bottom: 2px; }
.opt span { display: block; color: var(--text-3); font-size: .79rem; line-height: 1.5; }

.modal-foot {
  display: flex; align-items: center; gap: 10px;
  padding: 13px 20px; border-top: 1px solid var(--line); flex: none;
}
.modal-foot .spacer { flex: 1; }

.lnk {
  appearance: none; background: none; border: 0; cursor: pointer;
  color: var(--text-3); font-family: var(--font-mono); font-size: .74rem;
  display: inline-flex; align-items: center; gap: 6px; padding: 6px 4px;
}
.lnk:hover { color: var(--green); }
.lnk i, .lnk svg { width: 13px; height: 13px; }
.lnk[disabled] { opacity: .35; cursor: default; }

/* --- Wizard result ------------------------------------------------------- */

.verdict {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 16px; border-radius: var(--r);
  border: 1px solid var(--line); border-left: 3px solid var(--green);
  background: rgba(0,255,156,.06); margin-bottom: 18px;
}
.verdict i, .verdict svg { width: 22px; height: 22px; color: var(--green); flex: none; }
.verdict b { display: block; color: var(--text); font-size: .96rem; }
.verdict span { display: block; color: var(--text-2); font-size: .81rem; line-height: 1.5; margin-top: 2px; }

.verdict.warn { border-left-color: var(--amber); background: rgba(255,194,71,.06); }
.verdict.warn i, .verdict.warn svg { color: var(--amber); }
.verdict.danger { border-left-color: var(--red); background: rgba(255,77,109,.06); }
.verdict.danger i, .verdict.danger svg { color: var(--red); }

.spec { margin: 0 0 18px; }
.spec dt {
  font-family: var(--font-mono); font-size: .64rem; letter-spacing: .12em;
  text-transform: uppercase; color: var(--text-3); margin-top: 13px;
}
.spec dd { margin: 3px 0 0; color: var(--text); font-size: .88rem; }
.spec dd em { font-style: normal; color: var(--text-3); font-size: .82rem; }

.why { margin: 0 0 18px; padding: 0; list-style: none; }
.why li {
  position: relative; padding-left: 22px; margin-bottom: 8px;
  font-size: .83rem; color: var(--text-2); line-height: 1.6;
}
.why li::before {
  content: ""; position: absolute; left: 6px; top: .62em;
  width: 5px; height: 5px; border-radius: 50%; background: var(--green-4);
}

/* The generated prompt you paste straight into Claude Code. */
.prompt-out {
  position: relative;
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  background: var(--bg);
  padding: 14px 15px;
  font-family: var(--font-mono);
  font-size: .76rem;
  line-height: 1.65;
  color: var(--text-2);
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 260px;
  overflow-y: auto;
}

/* --- Reality check (can it be a website?) -------------------------------- */

.rc-list { display: grid; gap: 8px; margin: 0 0 18px; }

.rc-item {
  display: grid;
  grid-template-columns: 20px minmax(0, 1fr) auto;
  gap: 12px;
  align-items: center;
  padding: 11px 14px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  cursor: pointer;
  transition: border-color .16s, background .16s;
}

.rc-item:hover { border-color: var(--line-hot); }
.rc-item input { accent-color: var(--green); width: 15px; height: 15px; cursor: pointer; }
.rc-item b { display: block; color: var(--text); font-size: .86rem; font-weight: 500; }
.rc-item small { display: block; color: var(--text-3); font-size: .76rem; line-height: 1.45; margin-top: 2px; }

.rc-item:has(input:checked) { border-color: var(--line-hot); background: rgba(0,255,156,.05); }

/* Per-capability platform verdict pill. */
.pill {
  font-family: var(--font-mono); font-size: .6rem; letter-spacing: .08em;
  text-transform: uppercase; padding: 3px 8px; border-radius: 20px;
  border: 1px solid currentColor; white-space: nowrap;
}
.pill.ok   { color: var(--green); }
.pill.part { color: var(--amber); }
.pill.no   { color: var(--red); }

.rc-out {
  border-top: 1px solid var(--line);
  margin-top: 18px;
  padding-top: 18px;
}

/* --- Cost calculator ----------------------------------------------------- */

.calc-rows { display: grid; gap: 2px; margin-bottom: 16px; }

.calc-row {
  display: grid;
  grid-template-columns: 22px minmax(0, 1fr) auto;
  gap: 12px;
  align-items: center;
  padding: 10px 12px;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: background .16s;
}
.calc-row:hover { background: rgba(0,255,156,.04); }
.calc-row input[type="checkbox"], .calc-row input[type="radio"] { accent-color: var(--green); width: 15px; height: 15px; cursor: pointer; }
.calc-row.fixed { cursor: default; opacity: .95; }
.calc-row.fixed:hover { background: none; }
.calc-row b { display: block; color: var(--text); font-size: .85rem; font-weight: 500; }
.calc-row small { display: block; color: var(--text-3); font-size: .74rem; margin-top: 1px; }
.calc-row .amt { font-family: var(--font-mono); font-size: .84rem; color: var(--text-2); white-space: nowrap; font-variant-numeric: tabular-nums; }

.calc-total {
  display: flex; align-items: baseline; gap: 12px;
  padding: 15px 12px 4px; border-top: 1px solid var(--line);
}
.calc-total .lbl { font-family: var(--font-mono); font-size: .68rem; letter-spacing: .12em; text-transform: uppercase; color: var(--text-3); }
.calc-total .val { margin-left: auto; font-family: var(--font-mono); font-size: 1.7rem; color: var(--green); font-variant-numeric: tabular-nums; }
.calc-total .val small { font-size: .5em; color: var(--text-3); }

/* --- Layer diagram (frontend / backend) ---------------------------------- */

.layers { display: grid; gap: 8px; margin: 0 0 22px; }

.layer {
  display: grid;
  grid-template-columns: 92px minmax(0, 1fr);
  gap: 14px;
  align-items: center;
  padding: 13px 15px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
}

.layer .tag {
  font-family: var(--font-mono); font-size: .62rem; letter-spacing: .1em;
  text-transform: uppercase; color: var(--green-3); text-align: right;
}
.layer b { display: block; color: var(--text); font-size: .87rem; margin-bottom: 2px; }
.layer p { margin: 0; font-size: .79rem; color: var(--text-3); line-height: 1.5; }
.layer.you { border-color: var(--line-hot); background: rgba(0,255,156,.05); }
.layer.you .tag { color: var(--green); }

@media (max-width: 560px) {
  .layer { grid-template-columns: minmax(0, 1fr); gap: 4px; }
  .layer .tag { text-align: left; }
}

/* --- GFS backup diagram -------------------------------------------------- */

.gfs { margin: 0 0 8px; }
.gfs svg { width: 100%; height: auto; display: block; overflow: visible; }
.gfs .grid-line { stroke: var(--line); stroke-width: 1; }
.gfs .lbl { fill: var(--text-3); font-family: var(--font-mono); font-size: 9px; }
.gfs .lbl-b { fill: var(--text-2); font-family: var(--font-mono); font-size: 10px; }
.gfs .d  { fill: var(--green-4); }
.gfs .w  { fill: var(--green-3); }
.gfs .m  { fill: var(--green); }
.gfs .gone { fill: var(--line-2); }

/* Dots fade in left-to-right when the diagram scrolls into view. */
.gfs [data-pop] { opacity: 0; transform-box: fill-box; transform-origin: center; }
.gfs.in [data-pop] { animation: pop .42s cubic-bezier(.2,.9,.3,1.2) forwards; }
@keyframes pop { from { opacity: 0; transform: scale(.2) } to { opacity: 1; transform: none } }
@media (prefers-reduced-motion: reduce) {
  .gfs [data-pop] { opacity: 1; }
  .gfs.in [data-pop] { animation: none; }
}

.legend { display: flex; flex-wrap: wrap; gap: 16px; margin: 6px 0 22px; font-family: var(--font-mono); font-size: .72rem; color: var(--text-3); }
.legend span { display: inline-flex; align-items: center; gap: 7px; }
.legend i { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }

/* --- Do / don't columns -------------------------------------------------- */

.dodont { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; margin: 0 0 22px; }

.dd { padding: 15px 17px; border: 1px solid var(--line); border-radius: var(--r); background: var(--surface); }
.dd h5 { margin: 0 0 10px; font-size: .8rem; display: flex; align-items: center; gap: 8px; letter-spacing: 0; font-family: var(--font-display); }
.dd h5 i, .dd h5 svg { width: 15px; height: 15px; }
.dd.good h5 { color: var(--green); }
.dd.bad  h5 { color: var(--red); }
.dd ul { margin: 0; padding-left: 18px; }
.dd li { font-size: .81rem; color: var(--text-2); margin-bottom: 7px; line-height: 1.55; }
.dd.good li::marker { color: var(--green-4); }
.dd.bad  li::marker { color: var(--red); }

/* --- Checklist ----------------------------------------------------------- */
/* Ticks persist in localStorage so you can close the tab and come back. */

.check { display: grid; gap: 3px; margin: 0 0 22px; }

.check label {
  display: grid; grid-template-columns: 18px minmax(0, 1fr); gap: 12px;
  align-items: start; padding: 9px 12px; border-radius: var(--r-sm);
  cursor: pointer; transition: background .16s;
}
.check label:hover { background: rgba(0,255,156,.04); }
.check input { accent-color: var(--green); width: 15px; height: 15px; margin-top: 2px; cursor: pointer; }
.check b { display: block; color: var(--text); font-size: .85rem; font-weight: 500; }
.check small { display: block; color: var(--text-3); font-size: .77rem; line-height: 1.5; margin-top: 2px; }
.check input:checked ~ div b { color: var(--text-3); text-decoration: line-through; text-decoration-color: var(--green-4); }

.check-bar { display: flex; align-items: center; gap: 12px; margin: 0 0 14px; font-family: var(--font-mono); font-size: .72rem; color: var(--text-3); }
.check-bar .track { flex: 1; height: 4px; border-radius: 4px; background: var(--line); overflow: hidden; }
.check-bar .track > span { display: block; height: 100%; width: 0; background: var(--green); transition: width .3s ease-out; }

/* --- Misc ---------------------------------------------------------------- */

/* Inline callout linking to a launcher button mid-prose. */
.launch {
  display: flex; flex-wrap: wrap; align-items: center; gap: 14px;
  padding: 16px 18px; margin: 0 0 22px;
  border: 1px dashed var(--line-hot); border-radius: var(--r);
  background: rgba(0,255,156,.035);
}
.launch p { margin: 0; flex: 1 1 240px; font-size: .84rem; color: var(--text-2); line-height: 1.55; }

.kbd {
  font-family: var(--font-mono); font-size: .74rem;
  border: 1px solid var(--line-2); border-bottom-width: 2px;
  border-radius: 4px; padding: 1px 6px; color: var(--text);
  background: var(--bg-2); white-space: nowrap;
}

/* Money figures inside prose. */
.money { color: var(--green-2); font-family: var(--font-mono); font-size: .93em; }

table.ref td .money { font-size: 1em; }
table.ref .trap { color: var(--red); }
table.ref .good { color: var(--green); }

/* ==========================================================================
   Terminal casts
   A replay of a real recorded session. Hidden until the player has built it,
   so a reader with no JavaScript sees nothing rather than an empty frame —
   every command in a cast also appears in a <pre> next to it.
   ========================================================================== */

.cast { display: none; margin: 22px 0; }
.cast.ready { display: block; }

.cast-frame {
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  background: #050a08;
  overflow: hidden;
  box-shadow: var(--shadow);
}

.cast-bar {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 9px 12px;
  border-bottom: 1px solid var(--line);
  background: linear-gradient(180deg, rgba(255,255,255,.03), transparent);
}

.cast-bar i { width: 9px; height: 9px; border-radius: 50%; background: var(--line-2); flex: none; }
.cast-bar i:first-child { background: #3b2b2f; }

.cast-title {
  margin-left: 8px;
  font: 500 .76rem/1 var(--font-mono);
  color: var(--text-3);
  letter-spacing: .02em;
}

.cast-screen {
  margin: 0;
  padding: 14px 16px;
  background: none;
  border: 0;
  border-radius: 0;
  font: 400 .82rem/1.55 var(--font-mono);
  color: var(--text-2);
  white-space: pre-wrap;
  word-break: break-word;
  /* Tall enough that a replay does not shunt the page around as it types. */
  min-height: 15.5em;
  max-height: 26em;
  overflow-y: auto;
}

.cast-screen:focus-visible { outline: 2px solid var(--line-hot); outline-offset: -2px; }

.cast-ctl {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px 12px;
  border-top: 1px solid var(--line);
}

.cast-play {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  flex: none;
  padding: 0;
  border: 1px solid var(--line-2);
  border-radius: 50%;
  background: transparent;
  color: var(--text-2);
  cursor: pointer;
}

.cast-play:hover { color: var(--green); border-color: var(--line-hot); }
.cast-play > svg { width: 13px; height: 13px; }

.cast-track { flex: 1; height: 2px; background: var(--line-2); border-radius: 2px; overflow: hidden; }
.cast-track > span { display: block; width: 0; height: 100%; background: var(--green-4); }

.cast-time { font: 400 .72rem/1 var(--font-mono); color: var(--text-3); flex: none; }

.cast > figcaption,
.cast .cast-note {
  margin: 8px 2px 0;
  font-size: .78rem;
  color: var(--text-3);
}

/* Colours the recordings actually use. */
.cast-screen .c-dim { color: var(--text-3); }
.cast-screen .c-b   { color: var(--text); font-weight: 500; }
.cast-screen .c-grn { color: var(--green-3); }
.cast-screen .c-cyn { color: var(--cyan); }
.cast-screen .c-red { color: var(--red); }
.cast-screen .c-yel { color: var(--amber); }
.cast-screen .c-blu,
.cast-screen .c-mag { color: var(--green-2); }
.cast-screen .c-wht { color: var(--text); }

@media (max-width: 640px) {
  .cast-screen { font-size: .74rem; padding: 11px 12px; min-height: 13em; }
}

/* The commands, without the output — a recording you cannot copy out of is
   just a picture of a terminal. */
.cast-copy {
  flex: none;
  font-family: var(--font-mono);
  font-size: .66rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-3);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 5px 10px;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  transition: color .18s, border-color .18s;
}

.cast-copy:hover,
.cast-copy:focus-visible { color: var(--green); border-color: var(--line-hot); }
.cast-copy.ok { color: var(--green); border-color: var(--line-hot); }

@media (max-width: 640px) {
  .cast-copy { padding: 6px 9px; font-size: .62rem; }
}

/* The idle cursor. Same block as the hero's, sat after a heading once the
   reader has been still for a while. Decorative and aria-hidden, so it never
   reaches the accessibility tree. */
.idle-cursor {
  margin-left: 6px;
  opacity: .8;
  animation: blink 1.05s steps(2, start) infinite, cursor-in .25s ease-out;
}

@keyframes cursor-in { from { opacity: 0; transform: translateX(-4px); } }

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

/* ---------------------------------------------------------------------------
   Vendor marks

   Real logos, one <symbol> sprite per page, dropped in beside the name of the
   thing they belong to. They are single paths with no fill of their own, so
   they take the colour of the sentence they sit in — which is the only way
   they read on this palette. The official colour rides along as
   --brand-colour for anywhere pale enough to use it; nothing does yet.

   The class is not .brand: app.css has owned that since the nav was written.
   --------------------------------------------------------------------------- */
.vendor-sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.vendor-mark {
  width: 1em;
  height: 1em;
  fill: currentColor;
  flex: none;
  vertical-align: -.14em;
  opacity: .82;
}

/* A mark always precedes the name it belongs to, so the gap is always on the
   right. Only one rule sets it, so the two never stack. */
.vendor-mark { margin-right: .34em; }

/* A link is the usual home for a mark, and the link's own hover should carry
   the logo with it rather than leaving it dimmed behind. */
a:hover .vendor-mark, a:focus-visible .vendor-mark { opacity: 1; }

/* In an icon slot the mark is the slot's whole content: the slot sets the size
   and provides the gap, so the inline spacing has to come back off. */
.ico .vendor-mark { margin-right: 0; vertical-align: 0; opacity: 1; }

/* Standing alone in a card or a table cell, a mark is the label, so it gets
   the room and the weight of one. */
.vendor-solo {
  width: 1.5em;
  height: 1.5em;
  opacity: 1;
  vertical-align: -.32em;
}

/* Headings set the mark in the accent so it reads as part of the rule above
   it rather than as a word that lost its colour. */
h2 .vendor-mark, h3 .vendor-mark { color: var(--green-3); opacity: 1; }

/* --- Bumpers: the visualiser, between the parts -------------------------- */

/* A slim strip of the dedc MilkDrop visualiser at each part boundary. It is
   decoration, and it is treated as decoration: silent, looping, cropped to a
   letterbox, and masked into the page background at both ends so it reads as
   a divider rather than a pasted-in rectangle.

   The clip only plays when it is on screen and only when the visitor has not
   asked for less motion — see the bumper block in tutorial.js. Everything
   here has to look right with the video paused on its poster, because that is
   what a reduced-motion visitor sees permanently, and what everyone sees
   before the file is fetched. `preload="none"` means that is the common case,
   not the rare one. */
.bumper {
  position: relative;
  margin: 34px 0 30px;
  height: 132px;
  overflow: hidden;
  border-radius: 6px;
  background: var(--bg-1);
  /* Fade both ends into the page so the strip has no hard vertical edge. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}

.bumper video,
.bumper img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  opacity: .78;
}

/* A hairline along the top edge, the same gesture as `.part .line`, so the
   strip belongs to the same family as the part headings it sits between. */
.bumper::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--line-hot), transparent);
}

@media (max-width: 900px) { .bumper { height: 96px; } }
@media (max-width: 640px) { .bumper { height: 72px; margin: 24px 0 22px; } }

/* Nothing to disable for reduced motion: the script simply never calls play(),
   and a <video> that has not played shows its poster. The rule below is
   belt-and-braces for a browser that autoplays anyway despite the attribute
   being absent. */
@media (prefers-reduced-motion: reduce) {
  .bumper video { animation: none !important; }
}

/* ==========================================================================
   Magazine furniture
   The page was a tutorial with pictures in it. These are the pieces that
   make it read as an article: plates, spots, logo walls, pull quotes and
   the registrar cards. Everything here is decoration in the strict sense —
   remove the lot and not one instruction is lost — which is why every image
   is lazy, every caption is optional, and nothing below is load-bearing for
   a reader with images turned off.
   ========================================================================== */

/* --- Plates -------------------------------------------------------------- */
/* A plate is a wide illustration with a caption, sitting in the prose column
   the way a photograph sits in a magazine. The art is painted on the same
   near-black as the page, so `mix-blend-mode: screen` dissolves the edge and
   leaves the drawing floating — the same trick the part mascots use, and the
   same reason: screening near-black over near-black yields near-black, so
   there is no rectangle to see.

   The frame is drawn by the figure, not by the image, so the fade at the
   bottom can sit on top of the art and hand it off to the caption. */

figure.plate {
  margin: 30px 0 32px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background:
    radial-gradient(120% 140% at 50% 0%, rgba(0, 255, 156, .05), transparent 62%),
    var(--bg-1);
  overflow: hidden;
}

/* 12/5 is 2.4:1, which is exactly what tools/plates/gen.py asks the model
   for. Matching it means `object-fit: cover` never actually crops, so the
   composition that was chosen on the contact sheet is the composition that
   ships. An 8/3 frame looked better in the abstract and quietly cut a tenth
   off the top and bottom of every plate — which put the creature's feet on
   the caption. */
figure.plate > .frame {
  position: relative;
  display: block;
  aspect-ratio: 12 / 5;
  overflow: hidden;
}

figure.plate img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  mix-blend-mode: screen;
  opacity: .95;
}

/* The art fades into the caption bar rather than stopping at a hard line.
   Pointer-events off: it covers the picture and must not eat a click. */
figure.plate > .frame::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 34%;
  pointer-events: none;
  background: linear-gradient(180deg, transparent, var(--bg-1));
}

figure.plate figcaption {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  margin: -6px 0 0;
  padding: 0 18px 15px;
  font-size: .78rem;
  line-height: 1.55;
  color: var(--text-3);
}

figure.plate figcaption i,
figure.plate figcaption svg {
  width: 14px;
  height: 14px;
  flex: none;
  margin-top: 3px;
  color: var(--green-4);
}

figure.plate figcaption b { color: var(--text-2); font-weight: 500; }

/* A plate at the head of a section runs a little taller and loses its frame,
   so the part it introduces opens on a picture rather than on a box. */
figure.plate.opener {
  border: 0;
  background: none;
  margin: 6px 0 26px;
}

figure.plate.opener > .frame { border-radius: var(--r); }
figure.plate.opener > .frame::after { background: linear-gradient(180deg, transparent 55%, var(--bg)); }

/* No negative pull on an opener. The framed plate can tuck its caption up
   under the fade because the fade lands on a panel; an opener fades into the
   page itself, and the artwork is allowed to reach the bottom edge, so the
   caption needs real space rather than an overlap. */
figure.plate.opener figcaption { margin-top: 10px; padding-left: 2px; padding-right: 2px; }

/* The ratio is deliberately NOT changed on a phone. A taller frame would
   crop the ends off a composition whose whole point is that things are
   spread across the width — the two postboxes, the two cabinets, the shop
   and the archway. Better a shorter picture than half of one. */
@media (max-width: 640px) {
  figure.plate figcaption { font-size: .74rem; }
}

/* --- Spots --------------------------------------------------------------- */
/* A small square drawing that floats beside a paragraph. It is the lightest
   image on the page and the one that does the most for the feel of it: prose
   with a picture in the margin reads as an article, prose without reads as
   documentation.

   It floats on wide screens and becomes a centred band on narrow ones, where
   a 120px float would leave a column of two-word lines beside it. */
.spot {
  float: right;
  width: 132px;
  height: 132px;
  margin: 2px 0 12px 22px;
  mix-blend-mode: screen;
  opacity: .92;
  shape-outside: circle(52%);
}

.spot.left {
  float: left;
  margin: 2px 22px 12px 0;
}

@media (max-width: 760px) {
  .spot, .spot.left {
    float: none;
    display: block;
    width: 108px;
    height: 108px;
    margin: 4px auto 14px;
  }
}

/* --- Logo wall ----------------------------------------------------------- */
/* Tiles: one mark, one name, one line about what it is for. Used where a
   section names half a dozen tools in a row and a reader wants to know which
   of them they already recognise before reading a word. */
.logo-wall {
  display: grid;
  /* 250px, having been 168 and then 190. The prose column is 74ch — about
     640px, and never much more, because that is the cap. At 168 the grid fits
     four tiles across it, 150px each once the gaps are paid for, which turns
     every description into a five-word-wide well; the browser pass reported
     no overflow and no error for that, because nothing was broken, it was
     merely unreadable. 190 fixed the well and left a new problem: three
     columns orphan the fourth tile of a four-tile wall on its own row.
 
     250 lands on two columns at every width this column is ever given, so a
     four-tile wall is a 2x2 block and an eight-tile one is 2x4. Both read as
     a block rather than as a paragraph of cards, and each description gets
     ~300px, which is a real line of text. */
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 10px;
  margin: 20px 0 24px;
}

.logo-tile {
  display: grid;
  grid-template-areas: "ico" "name" "desc";
  gap: 6px;
  padding: 14px 14px 13px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  transition: border-color .2s, transform .2s;
}

.logo-tile > :first-child { grid-area: ico; }
.logo-tile b { grid-area: name; }
.logo-tile small { grid-area: desc; }

.logo-tile:hover { border-color: var(--line-hot); transform: translateY(-1px); }

.logo-tile .vendor-mark {
  width: 26px;
  height: 26px;
  margin: 0 0 2px;
  opacity: 1;
  color: var(--green-3);
  vertical-align: 0;
}

.logo-tile i, .logo-tile > svg:not(.vendor-mark) {
  width: 26px;
  height: 26px;
  color: var(--green-3);
}

.logo-tile b { font-size: .9rem; color: var(--text); font-weight: 600; }
.logo-tile small { font-size: .76rem; line-height: 1.5; color: var(--text-3); }

/* A tile can be a link. It keeps the tile's own colours rather than the
   page's link colour, because a wall of green names is a wall of noise. */
a.logo-tile, a.path { color: inherit; text-decoration: none; }
a.logo-tile:hover b, a.path:hover b { color: var(--green-2); }

/* A genuine inline link inside a tile's description keeps its underline —
   that one is a link in a sentence and should look like one. */
.logo-tile small a { color: var(--green-2); }

@media (max-width: 620px) {
  .logo-wall { grid-template-columns: 1fr; }

  /* One column at 390px is ~350px of text, which is fine. Putting the mark
     beside the words rather than above them keeps the tile short enough that
     four of them are still a glanceable block rather than a page of cards. */
  .logo-tile {
    grid-template-areas: "ico name" "ico desc";
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 12px;
    row-gap: 3px;
    align-items: start;
  }

  .logo-tile > :first-child { align-self: start; margin-top: 2px; }
}

/* --- Logo strip ---------------------------------------------------------- */
/* A single row of marks under a heading: "here is what this section is going
   to name". Purely a scanning aid, so it is aria-hidden in the markup and
   every name in it is repeated in the prose below. */
.logo-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 7px 9px;
  margin: -6px 0 22px;
  padding: 9px 12px;
  border: 1px solid var(--line);
  border-left: 2px solid var(--green-4);
  border-radius: var(--r-sm);
  background: rgba(0, 255, 156, .025);
}

.logo-strip .vendor-mark {
  width: 19px;
  height: 19px;
  margin: 0;
  opacity: .72;
  vertical-align: 0;
  color: var(--text-2);
  transition: opacity .2s, color .2s;
}

.logo-strip:hover .vendor-mark { opacity: 1; color: var(--green-3); }

.logo-strip .lbl {
  font-family: var(--font-mono);
  font-size: .68rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-right: 3px;
}

/* --- Pull quote ---------------------------------------------------------- */
/* One sentence, lifted out of the prose it already appears in, at a size you
   cannot skip. It never introduces a fact — a reader who reads only the pull
   quotes has missed nothing, which is the rule that keeps it decoration. */
.pullquote {
  margin: 30px 0;
  padding: 4px 0 4px 22px;
  border-left: 2px solid var(--green-4);
  font-size: clamp(1.05rem, 2.4vw, 1.42rem);
  line-height: 1.42;
  letter-spacing: -.015em;
  color: var(--green-2);
  font-weight: 500;
}

.pullquote small {
  display: block;
  margin-top: 10px;
  font-size: .78rem;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--text-3);
}

/* --- Drop cap ------------------------------------------------------------ */
/* On the first paragraph of a part only. Any more and it stops being a
   signal. `initial-letter` is not in every engine yet, so the float is the
   implementation and the modern property is not used at all — two sizing
   systems disagreeing is worse than one old one working everywhere. */
p.dropcap::first-letter {
  float: left;
  font-family: var(--font-mono);
  font-size: 3.1rem;
  line-height: .82;
  padding: 6px 10px 0 0;
  color: var(--green-3);
}

/* --- Registrar cards ----------------------------------------------------- */
/* The domain section branches by "where did you buy it", and a reader finds
   their own registrar by spotting the logo, not by reading seven names. Each
   card is a <details>: closed it is a logo and a name, open it is the four
   steps for that one registrar. Seven of them expanded at once would be a
   wall nobody reads. */
.reg-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 10px;
  margin: 20px 0 24px;
}

details.reg {
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  overflow: hidden;
}

details.reg[open] { border-color: var(--line-hot); }

details.reg > summary {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 14px;
  cursor: pointer;
  list-style: none;
  font-weight: 600;
  font-size: .92rem;
  color: var(--text);
}

details.reg > summary::-webkit-details-marker { display: none; }

details.reg > summary .vendor-mark {
  width: 22px;
  height: 22px;
  margin: 0;
  opacity: 1;
  color: var(--green-3);
  vertical-align: 0;
}

details.reg > summary .chev {
  margin-left: auto;
  width: 15px;
  height: 15px;
  color: var(--text-3);
  transition: transform .2s;
}

details.reg[open] > summary .chev { transform: rotate(180deg); }

details.reg > summary:hover { background: rgba(0, 255, 156, .04); }

details.reg .body {
  padding: 2px 15px 14px;
  border-top: 1px solid var(--line);
  font-size: .84rem;
  color: var(--text-2);
}

details.reg .body ol { margin: 12px 0 0; padding-left: 18px; }
details.reg .body li { margin-bottom: 7px; }
details.reg .body p { margin: 12px 0 0; }
details.reg .body code { font-size: .92em; }

/* The one fact that decides whether a transfer is worth doing at all, so it
   gets the top line of every open card rather than being buried in step 4. */
details.reg .body .verdict {
  display: block;
  margin: 12px 0 0;
  padding: 9px 11px;
  border-radius: var(--r-sm);
  background: rgba(0, 255, 156, .05);
  border-left: 2px solid var(--green-4);
  color: var(--text-2);
  font-size: .82rem;
}

/* --- Path chooser -------------------------------------------------------- */
/* Three doors at the top of the domain section. Not interactive: each is a
   link to the heading that answers it, so it works with no JavaScript and
   keeps working if the script ever fails to boot. */
.paths {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 12px;
  margin: 22px 0 26px;
}

a.path {
  display: block;
  padding: 16px 17px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--surface);
  color: inherit;
  transition: border-color .2s, transform .2s, background .2s;
}

a.path:hover {
  border-color: var(--line-hot);
  background: var(--surface-2);
  transform: translateY(-2px);
}

a.path .top {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-bottom: 9px;
}

a.path .top i, a.path .top svg { width: 19px; height: 19px; color: var(--green-3); flex: none; }

a.path .q {
  font-family: var(--font-mono);
  font-size: .68rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--green-4);
}

a.path b { display: block; font-size: 1rem; margin-bottom: 5px; color: var(--text); }
a.path p { margin: 0; font-size: .82rem; color: var(--text-3); line-height: 1.55; }

a.path .go {
  display: inline-block;
  margin-top: 11px;
  font-size: .8rem;
  color: var(--green-2);
  font-family: var(--font-mono);
}

/* --- Screenshot / diagram figure ----------------------------------------- */
/* For a picture that is evidence rather than decoration — a terminal, a
   dashboard, a diagram. Unlike a plate it keeps a hard edge, because the
   frame is part of the claim: this is a window, and what is in it was real. */
figure.shot {
  margin: 26px 0 28px;
  padding: 0;
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  background: var(--bg-2);
  overflow: hidden;
}

figure.shot .bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--line);
  background: var(--bg-1);
  font-family: var(--font-mono);
  font-size: .72rem;
  color: var(--text-3);
}

figure.shot .bar .dots {
  display: flex;
  gap: 5px;
  flex: none;
}

figure.shot .bar .dots span {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--line-2);
}

figure.shot img { display: block; width: 100%; height: auto; }
figure.shot figcaption {
  padding: 11px 14px;
  border-top: 1px solid var(--line);
  font-size: .78rem;
  color: var(--text-3);
}

/* --- Reduced motion ------------------------------------------------------ */
/* Nothing here animates on its own; the transitions above are all hover
   responses. Removing them under `reduce` is still correct — a person who
   asked for less movement did not exempt the bit under their cursor. */
@media (prefers-reduced-motion: reduce) {
  .logo-tile, a.path, details.reg { transition: none; }
  .logo-tile:hover, a.path:hover { transform: none; }
}

/* --- Charts --------------------------------------------------------------- */
/* Hand-rolled, like the cast player and for the same reason: the CSP allows a
   third-party script but not a third-party stylesheet, and a charting library
   would be several hundred kilobytes to draw four rectangles.
 
   The important design decision is that **the numbers are text**. The bars are
   `aria-hidden` decoration sitting behind a figure the reader can already
   read, so the chart survives a failed stylesheet, a screen reader, and a
   printer — the same rule as the scroll reveals, where a heading is content
   first and an effect second. Widths come from an inline `style` attribute,
   which the CSP allows and which means there is no JavaScript in the loop at
   all: nothing to boot, nothing to fail, nothing to animate into place. */

figure.chart {
  margin: 22px 0 24px;
  padding: 16px 18px 14px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--surface);
}

figure.chart > figcaption {
  font-size: .88rem;
  font-weight: 600;
  color: var(--text);
  margin-bottom: 3px;
}

figure.chart .sub {
  font-size: .78rem;
  color: var(--text-3);
  margin: 0 0 15px;
  line-height: 1.5;
}

.bars { display: grid; gap: 9px; }

.bar-row {
  display: grid;
  grid-template-columns: minmax(96px, auto) minmax(0, 1fr) 46px;
  align-items: center;
  gap: 12px;
}

.bar-lbl {
  font-size: .82rem;
  color: var(--text-2);
  display: flex;
  align-items: center;
  white-space: nowrap;
}

.bar-lbl .vendor-mark { opacity: 1; color: var(--green-3); }

.bar-track {
  height: 12px;
  border-radius: 3px;
  background: rgba(255, 255, 255, .035);
  overflow: hidden;
}

.bar-fill {
  display: block;
  height: 100%;
  border-radius: 3px;
  background: linear-gradient(90deg, var(--green-4), var(--green-3));
}

/* The leader gets the signal colour. Everything else is structural — the
   accent ramp's whole job on this site is to be used sparingly. */
.bar-row.lead .bar-fill { background: linear-gradient(90deg, var(--green-3), var(--green)); }
.bar-row.lead .bar-lbl { color: var(--text); font-weight: 600; }

/* Dimmed, for the row that is a caveat rather than a competitor — the
   three-quarters of servers that decline to say what they are. */
.bar-row.muted .bar-fill { background: var(--line-2); }
.bar-row.muted .bar-lbl, .bar-row.muted .bar-val { color: var(--text-3); }

.bar-val {
  font-family: var(--font-mono);
  font-size: .8rem;
  color: var(--green-2);
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.bar-row.muted .bar-val { color: var(--text-3); }

figure.chart .src {
  margin: 14px 0 0;
  padding-top: 11px;
  border-top: 1px solid var(--line);
  font-size: .74rem;
  line-height: 1.55;
  color: var(--text-3);
}

figure.chart .src a { color: var(--text-2); }
figure.chart .src a:hover { color: var(--green-2); }

/* At 390px a 96px label column plus a 46px value column leaves the bar about
   140px, which is still a readable bar — but the label wraps to two lines and
   drags the row apart. Stacking the label above the bar keeps each row one
   idea tall. */
@media (max-width: 560px) {
  .bar-row {
    grid-template-columns: minmax(0, 1fr) 46px;
    grid-template-areas: "lbl val" "bar bar";
    gap: 4px 10px;
  }
  .bar-lbl { grid-area: lbl; }
  .bar-val { grid-area: val; }
  .bar-track { grid-area: bar; }
}

/* ==========================================================================
   The live strip in the hero

   The claim in the paragraph above it is "there is a real machine at the
   other end of this". Four figures from that machine's own sampler are a
   better argument than the sentence is, so they sit directly under it.

   Every value here is rendered by the server before it is touched by
   JavaScript, using the same formatters app.js uses. The stream only ever
   moves a number that was already correct.
   ========================================================================== */

.hero-live {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "spark figs" "state figs";
  align-items: center;
  gap: 2px 20px;
  max-width: 640px;
  margin: 10px 0 0;
  padding: 12px 16px 10px;
  border: 1px solid var(--line);
  border-left: 2px solid var(--green-4);
  border-radius: var(--r-sm);
  background: rgba(0, 255, 156, .025);
}

.hl-spark { grid-area: spark; margin: 0; min-width: 0; }

.hl-spark svg {
  display: block;
  width: 100%;
  height: 44px;
  overflow: visible;
}

.hl-spark .line {
  fill: none;
  stroke: var(--green);
  stroke-width: 1.6;
  stroke-linejoin: round;
  stroke-linecap: round;
  /* The path is replaced wholesale on every frame, two seconds apart, so a
     transition here would be animating between two different shapes and
     would look like the line was breathing. It is left alone on purpose. */
}

.hl-spark .area { fill: rgba(0, 255, 156, .10); stroke: none; }

.hl-spark figcaption {
  margin-top: 3px;
  font-family: var(--font-mono);
  font-size: .64rem;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--text-3);
}

.hl-figs {
  grid-area: figs;
  display: flex;
  gap: 18px;
  margin: 0;
  text-align: right;
}

.hl-figs dt {
  font-family: var(--font-mono);
  font-size: .62rem;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--text-3);
}

.hl-figs dd {
  margin: 1px 0 0;
  font-family: var(--font-mono);
  font-size: 1.02rem;
  font-variant-numeric: tabular-nums;
  color: var(--green-2);
  white-space: nowrap;
}

.hl-figs dd i { font-style: normal; font-size: .7em; color: var(--text-3); margin-left: 1px; }

.hl-state {
  grid-area: state;
  font-family: var(--font-mono);
  font-size: .64rem;
  color: var(--text-3);
  display: flex;
  align-items: center;
  gap: 6px;
}

.hl-state::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--text-3);
  flex: none;
}

.hl-state[data-state="live"] { color: var(--green-3); }
.hl-state[data-state="live"]::before {
  background: var(--green);
  box-shadow: 0 0 0 0 rgba(0, 255, 156, .5);
  animation: hl-ping 2.4s ease-out infinite;
}

@keyframes hl-ping {
  0%   { box-shadow: 0 0 0 0 rgba(0, 255, 156, .45); }
  70%  { box-shadow: 0 0 0 7px rgba(0, 255, 156, 0); }
  100% { box-shadow: 0 0 0 0 rgba(0, 255, 156, 0); }
}

.hl-state[data-state="lost"] { color: var(--amber); }
.hl-state[data-state="lost"]::before { background: var(--amber); }

/* The stress-test strip under the live one. Same frame, quieter: these are
   figures from one dated run, not a live reading, so nothing here pulses. */
.hero-load {
  max-width: 640px;
  margin: 8px 0 0;
  padding: 11px 16px 12px;
  border: 1px solid var(--line);
  border-left: 2px solid var(--line-2);
  border-radius: var(--r-sm);
  background: rgba(255, 255, 255, .015);
}

.hload-head {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 0 0 8px;
  font-family: var(--font-mono);
  font-size: .64rem;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--text-3);
}

.hload-head i, .hload-head svg { width: 13px; height: 13px; color: var(--green-3); flex: none; }

.hload-figs {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px 16px;
  margin: 0;
}

.hload-figs dt {
  font-family: var(--font-mono);
  font-size: .6rem;
  line-height: 1.35;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-3);
}

.hload-figs dd {
  margin: 2px 0 0;
  font-family: var(--font-mono);
  font-size: 1.02rem;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  white-space: nowrap;
}

.hload-est {
  margin: 10px 0 0;
  font-size: .8rem;
  line-height: 1.5;
  color: var(--text-2);
}

.hload-est span { color: var(--text-3); }
.hload-est a { color: var(--green-2); white-space: nowrap; }

@media (max-width: 560px) {
  .hload-figs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* Below this the two columns are fighting over about 300px, and the figures
   win because they are the actual numbers. */
@media (max-width: 620px) {
  .hero-live {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "figs" "spark" "state";
    gap: 8px;
  }
  .hl-figs { text-align: left; justify-content: space-between; gap: 10px; }
  .hl-spark svg { height: 34px; }
}

/* ==========================================================================
   The landing loop, and the art on the cards

   Both are drawn on pure black and composited with `mix-blend-mode: screen`,
   the plates' trick: screening black over the page returns the page, so the
   picture floats with no box. tools/reels/publish.py and tools/cards/
   publish.py each floor the black and check the corners of the file they
   wrote, because a near-black corner is exactly the box this is avoiding.
   ========================================================================== */

.hero .wrap { position: relative; }
.hero .wrap > :not(.hero-reel) { position: relative; z-index: 1; }

/* Wide screens: the frame sits behind the right of the hero. Its left half is
   empty black by construction (the brief in tools/cards/cards.tsv asks for
   it), so the headline can overlap it and the screen blend makes that
   overlap invisible. Masked at the right and bottom so the cable that runs
   off the frame fades rather than stops. */
.hero-reel {
  position: absolute;
  top: -34px;
  right: 0;
  width: min(66%, 900px);
  pointer-events: none;
  mix-blend-mode: screen;
  -webkit-mask-image: linear-gradient(90deg, #000 88%, transparent), linear-gradient(#000 80%, transparent);
          mask-image: linear-gradient(90deg, #000 88%, transparent), linear-gradient(#000 80%, transparent);
  -webkit-mask-composite: source-in;
          mask-composite: intersect;
}

.hero-reel video {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1248 / 704;
}

/* Narrow screens have no empty right half to borrow, so the loop joins the
   flow above the headline and shows only the right of the frame, where the
   scene is. object-position keeps the creature and the cabinet; cover crops
   the black. */
@media (max-width: 980px) {
  .hero-reel {
    position: relative;
    top: auto;
    width: auto;
    /* Bleeds exactly the width of .wrap's padding, which is 24px here and
       18px below 760 (app.css). One pixel more and the page scrolls
       sideways — just shots caught 6px of it at 390. */
    margin: -20px -24px 4px;
    -webkit-mask-image: linear-gradient(#000 70%, transparent);
            mask-image: linear-gradient(#000 70%, transparent);
  }
  @media (max-width: 760px) { .hero-reel { margin-inline: -18px; } }
  .hero-reel video {
    aspect-ratio: 16 / 8;
    object-fit: cover;
    object-position: 88% 50%;
  }
}

/* The entrance. CSS only, `both` fill, and only when motion is welcome, so
   with animations removed every element is simply where it belongs — the
   heading is content first and an effect second. */
@media (prefers-reduced-motion: no-preference) {
  .hero .wrap > * { animation: hero-rise .7s cubic-bezier(.2, .7, .2, 1) both; }
  .hero .wrap > :nth-child(2) { animation-delay: .05s; }
  .hero .wrap > :nth-child(3) { animation-delay: .12s; }
  .hero .wrap > :nth-child(4) { animation-delay: .19s; }
  .hero .wrap > :nth-child(5) { animation-delay: .26s; }
  .hero .wrap > :nth-child(6) { animation-delay: .33s; }
  .hero .wrap > :nth-child(n+7) { animation-delay: .4s; }
  .hero .wrap > .hero-reel { animation: hero-glow 1.6s ease-out both; }
}

@keyframes hero-rise {
  from { opacity: 0; transform: translateY(14px); }
}

@keyframes hero-glow {
  from { opacity: 0; filter: blur(6px); }
}

/* The trailer: one wide strip under the hero, and the six parts it shows as
   links. The strip is screened onto the page like everything else drawn on
   black, and masked at both ends so the plates' empty margins melt into the
   backdrop instead of ending at a line. */
.trailer { padding: 4px 0 30px; }

.trailer-fig { margin: 0; max-width: 1100px; }

.trailer-frame {
  mix-blend-mode: screen;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}

.trailer-frame video {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1280 / 544;
}

.trailer-lbl {
  display: block;
  margin: 10px 0 8px;
  font-family: var(--font-mono);
  font-size: .68rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-3);
}

.trailer-parts {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.trailer-parts a {
  position: relative;
  display: block;
  height: 100%;
  padding: 8px 10px 10px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--text-2);
  font-size: .78rem;
  line-height: 1.3;
  text-decoration: none;
  overflow: hidden;
  transition: border-color .3s, color .3s, background .3s;
}

.trailer-parts .n {
  display: block;
  font-family: var(--font-mono);
  font-size: .64rem;
  color: var(--green-4);
  margin-bottom: 2px;
}

/* The part on screen now. A hairline along the bottom fills across the
   segment's own length — --seg is set by initTrailer() — so the list reads as
   a scrubber without being one. */
.trailer-parts a::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0;
  height: 2px;
  width: 100%;
  background: var(--green);
  transform: scaleX(0);
  transform-origin: left;
}

.trailer-parts li.on a { border-color: var(--line-hot); color: var(--text); background: var(--surface-2); }
.trailer-parts li.on a::after { animation: trailer-seg var(--seg, 4s) linear both; }
.trailer-parts a:hover { border-color: var(--line-hot); color: var(--text); }

@keyframes trailer-seg { to { transform: scaleX(1); } }

@media (max-width: 760px) {
  .trailer-parts { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .trailer-frame { margin-inline: -18px; }
}

/* Card art. One picture across the top of the card, bled to its edges with
   negative margins equal to the card's padding, dubloon-style. The width and
   height attributes on the tag only reserve the box; aspect-ratio decides the
   shape. */
.card-art {
  display: block;
  width: calc(100% + 2 * var(--pad-x));
  max-width: none;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  margin: calc(-1 * var(--pad-y)) calc(-1 * var(--pad-x)) 12px;
  border-radius: calc(var(--r) - 1px) calc(var(--r) - 1px) 0 0;
  border-bottom: 1px solid var(--line);
  mix-blend-mode: screen;
  transition: filter .3s;
}

.mini   { --pad-y: 15px; --pad-x: 16px; }
a.path  { --pad-y: 16px; --pad-x: 17px; }

.mini:has(.card-art),
a.path:has(.card-art) {
  transition: border-color .25s, transform .25s, box-shadow .25s, background .25s;
}

.mini:has(.card-art):hover,
a.path:has(.card-art):hover {
  border-color: var(--line-hot);
  transform: translateY(-3px);
  box-shadow: 0 12px 30px rgba(0, 0, 0, .55), 0 0 24px rgba(0, 255, 156, .08);
}

.mini:hover .card-art,
a.path:hover .card-art { filter: brightness(1.15) saturate(1.1); }

/* Four cards in a column that fits three leaves one alone on a row. Two by
   two instead, which also gives the four pictures that move the most room. */
.cards-mini.four { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 520px) { .cards-mini.four { grid-template-columns: minmax(0, 1fr); } }

/* The shopping list is a row, not a tile, so its picture is a thumbnail in
   the icon's place rather than a banner across the top. */
.part-card:has(.card-art) { grid-template-columns: 132px minmax(0, 1fr) auto; }
.part-card .card-art {
  width: 132px;
  margin: -6px 0;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
}
@media (max-width: 560px) {
  .part-card:has(.card-art) { grid-template-columns: 96px minmax(0, 1fr); }
  .part-card .card-art { width: 96px; }
}

@media (prefers-reduced-motion: reduce) {
  .mini:has(.card-art):hover,
  a.path:has(.card-art):hover { transform: none; }
}

/* ==========================================================================
   Mechanism diagrams

   Four of them: a name lookup, a request, a key handshake, a backup cycle.
   Each is one drawing and one numbered list saying the same thing, tied
   together by data-step — point at a box and its sentence lights up, point at
   a sentence and its box does.

   Three rules held throughout:

     · The drawing is aria-hidden and the list is not, because the list says
       everything the drawing says. There is no third place to look.
     · Below 700px the drawing is removed entirely. A 660-unit viewBox in a
       350px column renders 15px labels at eight pixels, which is a picture of
       some text rather than text — the failure this repo has now shipped
       twice and caught twice by screenshotting the component on its own.
     · The travelling dots are one CSS animation on a second copy of each
       path, so prefers-reduced-motion stops them the ordinary way. There is
       no SMIL and no script: a diagram with the animation removed is still a
       correct diagram.
   ========================================================================== */

figure.mech {
  margin: 26px 0 30px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: radial-gradient(120% 160% at 50% 0%, rgba(0, 255, 156, .045), transparent 60%), var(--bg-1);
  overflow: hidden;
}

.mech-head {
  padding: 12px 18px;
  border-bottom: 1px solid var(--line);
  font-size: .8rem;
  color: var(--text-3);
}

.mech-k {
  font-family: var(--font-mono);
  font-size: .68rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--green-3);
  margin-right: 8px;
}

.mech-stage { padding: 16px 14px 4px; }

.mech-stage svg { display: block; width: 100%; height: auto; }

/* --- boxes --- */

.mnode rect {
  fill: var(--surface-2);
  stroke: var(--line-2);
  stroke-width: 1;
  transition: stroke .18s, fill .18s;
}

.mnode text {
  text-anchor: middle;
  font-family: var(--font-display);
  fill: var(--text);
  transition: fill .18s;
}

.mnode .t { font-size: 14.5px; font-weight: 600; }
.mnode .s { font-size: 11px; fill: var(--text-3); font-family: var(--font-mono); }

.mnode.bad rect { stroke: rgba(255, 77, 109, .38); }
.mnode.bad .t { fill: var(--red); }

/* --- wires --- */

.mwire .wire, .tip {
  fill: none;
  stroke: var(--line-2);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: stroke .18s;
}

.tip { fill: none; }

.mwire .w {
  text-anchor: middle;
  font-family: var(--font-mono);
  font-size: 11px;
  fill: var(--text-3);
  transition: fill .18s;
}

/* The travelling dot. pathLength="100" normalises every path in every
   diagram to the same 0..100 space, so one keyframe drives a 90px wire and a
   700px one at the same apparent speed, and the delays in the markup are
   directly comparable. */
.mwire .pulse {
  fill: none;
  stroke: var(--green);
  stroke-width: 5;
  stroke-linecap: round;
  stroke-dasharray: 1 99;
  animation: mech-flow 3.4s linear infinite;
}

@keyframes mech-flow {
  from { stroke-dashoffset: 0; }
  to   { stroke-dashoffset: -100; }
}

.mwire.answer .pulse { stroke: var(--cyan); }
.mwire.go .pulse     { stroke: var(--green); stroke-width: 6; animation-duration: 2.6s; }
.mwire.go .wire      { stroke: var(--green-4); }
.tip.go              { stroke: var(--green-4); }
.tip.answer          { stroke: var(--line-2); }
.tip.bad             { stroke: rgba(255, 77, 109, .5); }

.mwire.blocked .wire { stroke: rgba(255, 77, 109, .45); stroke-dasharray: 4 4; }
.mwire.blocked .pulse { stroke: var(--red); animation-duration: 1.6s; }

/* --- the enclosure and the refusal mark --- */

.mbox rect {
  fill: none;
  stroke: var(--green-4);
  stroke-width: 1;
  stroke-dasharray: 6 5;
  opacity: .7;
}

.mbox text {
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: .1em;
  text-transform: uppercase;
  fill: var(--green-4);
}

.mno path { stroke: var(--red); stroke-width: 2.2; stroke-linecap: round; fill: none; }
.mno text { font-family: var(--font-mono); font-size: 11px; fill: var(--red); opacity: .85; }

.life { stroke: var(--line); stroke-width: 1; stroke-dasharray: 3 5; fill: none; }

/* --- the list, which is the diagram in words --- */

/* Specificity, not preference: .prose ol sets padding-left: 22px, which beats
   a single class and pulled the numbers under this figure's clipped edge.
   The markers were rendering the whole time — just outside the box. */
.prose .mech-steps {
  margin: 6px 0 0;
  padding: 0 18px 8px 46px;
  font-size: .84rem;
  line-height: 1.6;
  color: var(--text-2);
}

.prose .mech-steps li {
  margin: 0 0 7px;
  padding: 3px 9px;
  border-radius: var(--r-sm);
  transition: background .18s, color .18s;
}

.mech-steps li::marker { color: var(--green-4); font-family: var(--font-mono); font-size: .82em; }
.mech-steps li code { font-size: .92em; }

.mech-cap {
  margin: 0;
  padding: 12px 18px 14px;
  border-top: 1px solid var(--line);
  font-size: .8rem;
  color: var(--text-3);
  font-style: italic;
}

/* --- the tie between the two halves --- */

figure.mech.lit .mnode:not(.on) rect { opacity: .45; }
figure.mech.lit .mnode:not(.on) text { opacity: .45; }
figure.mech.lit .mwire:not(.on)      { opacity: .3; }
figure.mech.lit .tip:not(.on)        { opacity: .3; }

.mnode.on rect { stroke: var(--green); fill: rgba(0, 255, 156, .09); }
.mnode.on .t   { fill: var(--green-2); }
.mwire.on .wire { stroke: var(--green-3); }
.mwire.on .w    { fill: var(--green-2); }
.tip.on         { stroke: var(--green-3); }

.mech-steps li.on {
  background: rgba(0, 255, 156, .07);
  color: var(--text);
}

/* No drawing on a phone — see the third rule at the top of this block. The
   list is not a consolation prize, it is the same content set in type that
   can actually be read at arm's length. */
@media (max-width: 700px) {
  .mech-stage { display: none; }
  .prose .mech-steps { margin-top: 12px; padding-left: 40px; }
}

@media (prefers-reduced-motion: reduce) {
  /* The dot parks at the start of its wire, which is a point on the wire —
     so the drawing still reads as a drawing rather than as one with a bug. */
  .mwire .pulse { animation: none; }
}

/* ==========================================================================
   Plate parallax

   Scroll-driven, so it is the browser's compositor moving the picture and
   not a scroll listener — there is no JavaScript in this at all, and on a
   browser without animation-timeline the plates are exactly what they were.

   scale(1.08) with a 3% travel is the whole budget, and it is deliberately
   small. tools/plates/gen.py asks for 12/5 and the frame is 12/5 precisely so
   that object-fit: cover never crops; an 8/3 frame was tried once and quietly
   put the creature's feet on the caption. This crops 4% at rest, which the
   composition can carry, and nothing beyond that.
   ========================================================================== */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    figure.plate img {
      animation: plate-drift linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }

    @keyframes plate-drift {
      from { transform: translateY(-3%) scale(1.08); }
      to   { transform: translateY(3%)  scale(1.08); }
    }
  }
}

/* ==========================================================================
   The VPS market survey
   Rendered from web/data/market.json, so every cell here is a dated fact with
   a source. Two shapes: a table of numbers, and a disclosure per provider for
   the prose about it — because prose goes UNDER a table and never inside one.
   That rule was learned the hard way twice; see docs/handoff.md.
   ========================================================================== */

figure.chart.survey { padding-bottom: 4px; }

/* The table sits flush inside the figure, which already has the border. */
figure.chart.survey .tbl-wrap {
  margin: 0;
  border-radius: var(--r-sm);
}

/* Provider cells carry a plan name under the company name, so the first
   column stops being the single-line monospace key the base style assumes. */
table.ref.market td:first-child {
  white-space: normal;
  font-family: var(--font);
  font-size: .82rem;
  min-width: 148px;
}

table.ref.market td b { color: var(--text); font-weight: 600; }

table.ref.market td small {
  display: block;
  margin-top: 3px;
  color: var(--text-3);
  font-size: .71rem;
  line-height: 1.45;
  font-weight: 400;
  letter-spacing: 0;
}

/* The annual figure is the column the section argues from, so it sits second
   — beside the provider — rather than last. At 390px the table scrolls, and
   whatever is off the right edge may as well not exist: the first draft put
   the price there and a phone showed nine providers with no prices. It also
   carries no prose, because a sentence in this cell ("long intro term, then
   $11.99/mo") widened the column until the number itself was clipped at
   1440px. The billing terms live on the card below instead. */
table.ref.market td.yr {
  font-family: var(--font-mono);
  font-size: .86rem;
  white-space: nowrap;
}

table.ref.market td.yr small { font-family: var(--font); }

/* "38 GB SSD RAID-10" in a narrow column becomes five lines of one word,
   which is the same unreadable-but-not-broken failure as the relay table in
   #email. These two hold their line and let the wrapper scroll instead. */
table.ref.market td.spec { white-space: nowrap; }

table.ref.market .pick { margin-left: 6px; }
/* The other disclosure badges (a row's `tie`). Amber, not green: "this box" is
   the page's own machine and wears the page's colour; a friend's company or
   the author's other box is a different kind of interest and should not read
   as the same one. */
.pick.tie { background: var(--amber); white-space: nowrap; }
table.ref.market td.yr.trap small { color: var(--text-3); }

/* The row this site is served from. Marked in the table rather than in a
   footnote, because a conflict of interest declared quietly is not declared. */
table.ref.market tr.ours { background: rgba(0, 255, 156, .05); }
table.ref.market tr.ours:hover { background: rgba(0, 255, 156, .07); }
table.ref.market tr.ours td:first-child b { color: var(--green-2); }

/* --- One disclosure per provider ----------------------------------------- */

.survey-notes { display: grid; gap: 8px; margin: 0 0 22px; }

details.reg.mkt > summary { gap: 8px; flex-wrap: wrap; }

details.reg.mkt > summary .plan {
  color: var(--text-3);
  font-weight: 400;
  font-size: .82rem;
}

details.reg.mkt > summary .yr {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: .8rem;
  color: var(--green-2);
}

details.reg.mkt > summary .yr.trap { color: var(--red); }
details.reg.mkt > summary .chev { margin-left: 0; }

details.reg .body p.meta {
  display: flex;
  align-items: flex-start;
  gap: 7px;
  margin: 9px 0 0;
  font-size: .77rem;
  color: var(--text-3);
  line-height: 1.5;
}

details.reg .body p.meta svg {
  width: 13px;
  height: 13px;
  flex: 0 0 auto;
  margin-top: 3px;
  color: var(--green-4);
}

details.reg .body p.meta a { color: var(--text-2); word-break: break-word; }
details.reg .body p.meta a:hover { color: var(--green-2); }

/* At phone width the annual figure has nowhere to go on the summary line, so
   it drops under the name instead of squeezing the plan into two words. */
@media (max-width: 560px) {
  details.reg.mkt > summary .plan { flex: 1 1 100%; order: 3; }
  details.reg.mkt > summary .yr { order: 2; }
}

/* ==========================================================================
   The decision chart
   Named .dchart and not .flow: guide.css already has a .flow, a horizontal
   node-and-arrow diagram for the handbook, and both stylesheets load on both
   pages. The first draft called this .flow and silently inherited that
   component's display:flex — which is also how the reveal bug below was
   found, because a flex container reported a height nothing else explained.
   The whole tree is in the markup: every question and every ending is server
   rendered and present in the DOM. Without JavaScript the reader sees all of
   it at once, which is a perfectly good infographic — `html.js-ready` is what
   switches it into one-step-at-a-time mode, the same stamp the scroll-reveal
   failsafe uses. So the feature degrades to "more visible", never to "empty".
   ========================================================================== */

.dchart {
  position: relative;
  margin: 24px 0 26px;
  padding: 0 0 0 26px;
}

/* The spine. A gradient rather than a flat line so a long chart does not read
   as a table border, and drawn with a pseudo-element so it costs no markup. */
.dchart::before {
  content: "";
  position: absolute;
  left: 7px; top: 8px; bottom: 8px;
  width: 2px;
  border-radius: 2px;
  background: linear-gradient(180deg, var(--green-4), var(--line) 70%, transparent);
}

.dchart-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin: 0 0 16px;
  min-height: 30px;
}

.ftrail { display: flex; flex-wrap: wrap; gap: 6px; flex: 1 1 200px; }

.ftrail button {
  font-family: var(--font-mono);
  font-size: .68rem;
  letter-spacing: .04em;
  color: var(--text-2);
  background: rgba(0, 255, 156, .07);
  border: 1px solid var(--line-hot);
  border-radius: 20px;
  padding: 3px 10px;
  cursor: pointer;
}

.ftrail button:hover { color: var(--green-2); border-color: var(--green-4); }

.dchart-acts { display: flex; gap: 8px; }

.fbtn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: .69rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-3);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 6px 11px;
  cursor: pointer;
}

.fbtn:hover { color: var(--green-2); border-color: var(--line-hot); }
.fbtn svg { width: 13px; height: 13px; }
.fbtn.on { color: var(--green-2); border-color: var(--green-4); background: rgba(0,255,156,.06); }

/* Every step is visible by default — that is the no-JavaScript infographic. */
.fstep { position: relative; margin: 0 0 18px; }

/* The node dot on the spine. */
.fstep::before {
  content: "";
  position: absolute;
  left: -24px; top: 14px;
  width: 10px; height: 10px;
  border-radius: 50%;
  background: var(--bg);
  border: 2px solid var(--green-4);
}

.fstep.fend::before { background: var(--green); border-color: var(--green); }

.fq { display: flex; gap: 12px; align-items: flex-start; margin: 0 0 12px; }

.fq .qn {
  font-family: var(--font-mono);
  font-size: .68rem;
  letter-spacing: .1em;
  color: var(--green-4);
  padding-top: 3px;
}

.fq b { display: block; font-size: 1rem; color: var(--text); line-height: 1.4; }

.fq p {
  margin: 5px 0 0;
  font-size: .82rem;
  color: var(--text-3);
  line-height: 1.55;
}

/* --- The options ---------------------------------------------------------- */

.fopts { display: grid; gap: 8px; }

.fopt {
  display: flex;
  align-items: flex-start;
  gap: 11px;
  width: 100%;
  text-align: left;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  cursor: pointer;
  color: var(--text-2);
  font: inherit;
  transition: border-color .15s, background .15s, transform .15s;
}

.fopt:hover {
  border-color: var(--line-hot);
  background: rgba(0, 255, 156, .04);
  transform: translateX(2px);
}

.fopt > svg {
  width: 16px; height: 16px;
  flex: 0 0 auto;
  margin-top: 2px;
  color: var(--green-3);
}

.fopt b { display: block; font-size: .88rem; color: var(--text); font-weight: 600; line-height: 1.4; }

.fopt small {
  display: block;
  margin-top: 3px;
  font-size: .77rem;
  color: var(--text-3);
  line-height: 1.5;
}

.fopt.chosen { border-color: var(--green-4); background: rgba(0, 255, 156, .07); }
.fopt.chosen > svg { color: var(--green); }
.fopt.joke:hover { border-color: var(--red); background: rgba(255, 80, 80, .05); }
.fopt.joke:hover > svg { color: var(--red); }

/* --- An ending ------------------------------------------------------------ */

.fverdict {
  border: 1px solid var(--line-hot);
  border-radius: var(--r);
  background: var(--surface);
  overflow: hidden;
}

.fverdict.good { border-color: var(--green-4); }
.fverdict.warn { border-color: #6b5a1e; }
.fverdict.joke { border-color: var(--line); }

.vhead {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 14px 16px;
  background: rgba(0, 255, 156, .05);
  border-bottom: 1px solid var(--line);
}

.fverdict.warn .vhead { background: rgba(255, 200, 60, .05); }
.fverdict.joke .vhead { background: rgba(255, 255, 255, .02); }

.vhead > svg { width: 20px; height: 20px; flex: 0 0 auto; margin-top: 2px; color: var(--green-2); }
.fverdict.warn .vhead > svg { color: #e0b64a; }

.vtag {
  display: block;
  font-family: var(--font-mono);
  font-size: .62rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-bottom: 3px;
}

.vhead b { display: block; font-size: 1rem; color: var(--text); line-height: 1.35; }

.vbody { padding: 4px 16px 16px; }
.vbody p { font-size: .86rem; color: var(--text-2); line-height: 1.7; margin: 14px 0 0; }
.vbody .notice { margin: 16px 0 0; }
.vbody .fopts { margin-top: 16px; }

/* --- What to build it with ------------------------------------------------ */

.fkit {
  display: grid;
  gap: 10px;
  margin: 16px 0 0;
  padding-top: 15px;
  border-top: 1px solid var(--line);
}

.fk {
  display: grid;
  grid-template-columns: 128px minmax(0, 1fr);
  gap: 14px;
  align-items: baseline;
}

.fkl {
  font-family: var(--font-mono);
  font-size: .68rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--green-4);
}

.fk p { margin: 0; font-size: .82rem; color: var(--text-2); line-height: 1.65; }

/* Two columns of 128px and prose at 390px leaves a five-word well, which is
   the readability failure this repo has now shipped three times. Below 620px
   the label goes above its paragraph instead. */
@media (max-width: 620px) {
  .fk { grid-template-columns: 1fr; gap: 3px; }
}

/* --- Live mode: only with JavaScript ------------------------------------- */

html.js-ready .dchart:not(.dchart-all) .fstep:not(.on) { display: none; }

/* The map button shows everything again, which is also the no-script state.
   Nothing in here is a code path that can be missed, because the "feature" is
   a single class on one element. */
html.js-ready .dchart.dchart-all .fstep { display: block; }
html.js-ready .dchart.dchart-all .fopts { opacity: .75; }

@media (max-width: 560px) {
  .dchart { padding-left: 20px; }
  .dchart::before { left: 4px; }
  .fstep::before { left: -21px; }
  .dchart-acts { width: 100%; }
  .fbtn { flex: 1; justify-content: center; }
}

/* ==========================================================================
   The language map
   An SVG picture of the cards below it, built by tutorial.js from the cards'
   own data attributes. Below 700px the picture is hidden and the cards carry
   the section alone — the same trade the mechanism diagrams make, for the
   same reason: eighteen labelled nodes in 354px is a diagram of nothing.
   ========================================================================== */

.lgraph {
  margin: 22px 0 18px;
  padding: 14px 10px 10px;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--surface);
}

.lg-canvas { width: 100%; }

.lg-hint {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 10px 0 0;
  padding-top: 10px;
  border-top: 1px solid var(--line);
  font-size: .75rem;
  color: var(--text-3);
}

.lg-hint svg { width: 13px; height: 13px; color: var(--green-4); }

.lg-collabel {
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: .12em;
  text-transform: uppercase;
  fill: var(--green-4);
}

.lg-edge {
  fill: none;
  stroke: var(--line-2);
  stroke-width: 1.2;
  transition: stroke .18s, opacity .18s;
}

.lg-box {
  fill: var(--bg-2);
  stroke: var(--line-2);
  stroke-width: 1;
  transition: fill .18s, stroke .18s;
}

.lg-name {
  font-family: var(--font-mono);
  font-size: 11px;
  fill: var(--text-2);
  pointer-events: none;
  transition: fill .18s;
}

.lg-node { cursor: pointer; }
.lg-node:hover .lg-box { stroke: var(--green-4); fill: rgba(0, 255, 156, .06); }
.lg-node:hover .lg-name { fill: var(--text); }

/* With something selected, everything unrelated steps back. The dimming is
   opacity on the shapes rather than a redraw, so a browser that ignores the
   transition still lands on the correct final state. */
.lg-svg.has-sel .lg-node { opacity: .32; }
.lg-svg.has-sel .lg-edge { opacity: .18; }
.lg-svg.has-sel .lg-node.sel,
.lg-svg.has-sel .lg-node.adj { opacity: 1; }
.lg-svg.has-sel .lg-edge.lit { opacity: 1; stroke: var(--green-4); }

.lg-node.sel .lg-box { fill: rgba(0, 255, 156, .14); stroke: var(--green); }
.lg-node.sel .lg-name { fill: var(--green-2); }
.lg-node.adj .lg-box { stroke: var(--green-4); }

@media (max-width: 700px) {
  .lgraph { display: none; }
}

/* --- The cards, which are the actual content ----------------------------- */

.langs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 12px;
  margin: 0 0 22px;
}

.lang {
  padding: 14px 15px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  scroll-margin-top: 90px;
  transition: border-color .18s, background .18s;
}

.lang:hover { border-color: var(--line-hot); }
.lang.sel { border-color: var(--green); background: rgba(0, 255, 156, .05); }

.lang > header {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 10px;
  padding-bottom: 9px;
  border-bottom: 1px solid var(--line);
}

.lang > header b { font-size: .95rem; color: var(--text); }
.lang > header .vendor-mark { width: 17px; height: 17px; margin: 0; opacity: 1; color: var(--green-3); vertical-align: 0; }

.lang > header .lwhere {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: .64rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-3);
}

.lang p { margin: 0 0 8px; font-size: .81rem; line-height: 1.65; color: var(--text-2); }
.lang p:last-child { margin-bottom: 0; }

.lang .ll {
  display: inline-block;
  min-width: 58px;
  margin-right: 6px;
  font-family: var(--font-mono);
  font-size: .64rem;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--green-4);
}

/* The criticism is the part that makes the rest trustworthy, so it is marked
   rather than buried as the third paragraph of three. */
.lang p:last-child .ll { color: var(--text-3); }
