/* ═══════════════════════════════════════════════════════════════════════════
   BLOCKPARTY 437 — a design system made of code page 437.
   One stylesheet, no build step, no dependencies, no JavaScript required.

   ── INSTALL ────────────────────────────────────────────────────────────────
   1. <link rel="stylesheet" href="blockparty.css">
   2. Load the three faces (or drop this and it falls back to your system mono):
      <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=
      JetBrains+Mono:wght@400;500;700&family=Martian+Mono:wght@500;700;800&
      family=Silkscreen:wght@400;700&display=swap">
   3. Put class="bp" on <body> (or on any subtree you want the system to own).
   4. Optional: data-palette="blueprint" on <html> to switch the whole thing.

   ── HOW IT IS ORGANISED ────────────────────────────────────────────────────
   §1  Tokens          15 custom properties. Change these, change everything.
   §2  Palettes        9 complete swaps of those 15. Ground stays white.
   §3  Base            typography, the three type roles, focus.
   §4  Layout          row / stack / grid, all on a character grid.
   §5  Panel           the framed box, with a notched title.
   §6  Buttons         brackets are the chrome.
   §7  Fields          input, select, [x] checkbox, (o) radio.
   §8  Status          pills that never rely on colour alone.
   §9  Table           the one place hairlines beat characters.
   §10 Data            charts built from twelve characters.
   §11 Recall          prompt / spoiler / verdict card.
   §12 Appendix        ~40 lines of optional JS: real character rails + the
                       dissolve and shred animations.

   ── THE SIX RULES ──────────────────────────────────────────────────────────
   1. Earn the density. Use this where density is the point — logs, diffs,
      queues, infra, telemetry. Not for a marketing page.
   2. Don't cosplay the past. No CRT curvature, no fake boot sequence, no
      ASCII where a real control belongs. Borrow the grid, not the museum.
   3. Characters first, hairlines second. Draw with glyphs wherever they hold
      alignment; fall back to a 1px rule only where a glyph fights the layout.
   4. Never let a glyph be the only signal. Every state ships as glyph + word
      + colour. Drop any one of the three and the row still reads.
   5. One raised object per screen. Exactly one filled button with a shadow.
   6. Spend the motion budget once. Three moving things on a screen is the
      ceiling, and every one of them stops at prefers-reduced-motion.

   Measurements are in `ch` and `em`, never px — one character cell is the
   atom. Gutter 2ch, panel inset 2ch, prose measure 66ch.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ORBIT — the console layer from the prototype. */
html,body{height:100%}body{margin:0}.orb{display:grid;grid-template-columns:32ch minmax(0,1fr);min-height:100vh}
.rail{border-right:1px solid var(--bp-rule);padding:1.1rem 2ch 3rem;position:sticky;top:0;height:100vh;overflow:auto;box-sizing:border-box}
.bp a.rail__mark{font-family:'Martian Mono','JetBrains Mono',monospace;font-weight:800;font-size:.82rem;letter-spacing:-.05em;display:block;margin-bottom:.2rem;color:var(--bp-ink);text-decoration:none}.rail__mark b{color:var(--bp-accent)}
.rail__host{font-size:.68rem;color:var(--bp-ink-3);display:block;margin-bottom:1.4rem;line-height:1.5}.rail__grp{margin:1.3rem 0 .4rem}.project-head{display:flex;align-items:baseline;gap:1ch}.add-project{margin-left:auto!important;color:var(--bp-accent)!important}
.navlist{display:grid;gap:0;font-size:.78rem}.bp a.nav{all:unset;cursor:pointer;display:flex;gap:1ch;align-items:baseline;padding:.12rem 1ch .12rem 0;color:var(--bp-ink-2);white-space:nowrap;text-decoration:none}.nav .tw{color:var(--bp-rule);flex:none}.nav .st{margin-left:auto;flex:none}.bp a.nav:hover{color:var(--bp-accent)}.bp a.nav[aria-current="page"]{color:var(--bp-accent);font-weight:700}.nav[aria-current="page"] .tw{color:var(--bp-accent)}.nav:focus-visible{outline-offset:-2px}
.col{min-width:0;display:flex;flex-direction:column}.hd{display:flex;gap:2.5ch;align-items:center;flex-wrap:wrap;padding:.65rem 2.5ch;border-bottom:1px solid var(--bp-rule);position:sticky;top:0;background:var(--bp-paper);z-index:20}.hd__crumb{font-size:.76rem;color:var(--bp-ink-3)}.hd__crumb b{color:var(--bp-ink);font-weight:500}.hd .lock{margin-left:auto}.main{padding:1.8rem 2.5ch 6rem;min-width:0;flex:1;outline:0}.view{display:grid;grid-template-columns:minmax(0,1fr);gap:2.4rem}.h{display:flex;gap:2ch;align-items:baseline;flex-wrap:wrap;margin-bottom:.9rem}.h h2{margin:0}.h .sub{font-size:.74rem;color:var(--bp-ink-3)}.h .spacer{margin-left:auto}
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(20ch,1fr));gap:0}.kpi{padding:.7rem 2ch;border-left:1px dotted var(--bp-rule);display:grid;gap:.1rem;align-content:start}.kpi:first-child{border-left:none;padding-left:0}.kpi b{font-family:'Martian Mono','JetBrains Mono',monospace;font-size:1.45rem;font-weight:800;line-height:1.15;font-variant-numeric:tabular-nums}.kpi b span{font-size:.72rem;font-weight:500}.kpi .spark{font-size:.85rem;letter-spacing:-.04em;color:var(--bp-accent)}.kpi .foot{font-size:.68rem;color:var(--bp-ink-3)}
.empty{padding:2rem 1ch;display:grid;gap:1rem;justify-items:start}.empty__mark{font-family:'Martian Mono',monospace;font-size:4rem;line-height:.8;color:var(--bp-paper-3)}
.kinds,.sizes{display:grid;grid-template-columns:repeat(auto-fit,minmax(21ch,1fr));gap:0}.kind,.size{all:unset;cursor:pointer;padding:1rem 2ch;border-left:1px dotted var(--bp-rule);display:grid;gap:.4rem;align-content:start}.kind:first-child,.size:first-child{border-left:none}.kind:hover,.size:hover{background:var(--bp-paper-2)}.kind[aria-pressed="true"],.size[aria-pressed="true"]{background:var(--bp-accent);color:var(--bp-paper)}.kind[aria-pressed="true"] small,.size[aria-pressed="true"] small{color:var(--bp-paper);opacity:.85}.kind b,.size b{font-family:'Martian Mono',monospace;font-weight:800;font-size:.95rem;letter-spacing:-.03em}.kind small,.size small{font-size:.7rem;color:var(--bp-ink-3)}
.size[hidden],.static-size[hidden]{display:none}.static-size{padding:1rem 2ch;background:var(--bp-accent);color:var(--bp-paper);display:grid;gap:.4rem}.static-size b{font-family:'Martian Mono',monospace;font-size:.95rem}.static-size small{font-size:.7rem;opacity:.85}.resource-summary{display:flex;align-items:center;justify-content:space-between;gap:1rem 2ch;flex-wrap:wrap;margin-top:1rem;padding-top:1rem;border-top:1px dotted var(--bp-rule)}.resource-summary>span{display:grid;gap:.25rem}.resource-summary>span>b{font-size:.78rem;font-variant-numeric:tabular-nums}.quota-editor{display:grid;gap:.8rem;margin:1rem 0;padding:1rem 2ch;background:var(--bp-paper-2);border-left:3px solid var(--bp-second)}.quota-editor[hidden]{display:none}.sl{display:grid;grid-template-columns:12ch minmax(0,1fr) 14ch;gap:.5rem 2ch;align-items:center;font-size:.78rem}.sl[hidden]{display:none}.sl .k{color:var(--bp-ink-3)}.sl input[type=range]{width:100%;accent-color:var(--bp-accent);background:transparent}.sl .v{text-align:right;font-variant-numeric:tabular-nums}.sl .v b{color:var(--bp-accent)}
.form{display:grid;grid-template-columns:repeat(auto-fit,minmax(28ch,1fr));gap:1.4rem 3ch}.cap{display:grid;grid-template-columns:14ch minmax(0,1fr) 18ch;gap:1ch 2ch;align-items:baseline;font-size:.78rem}.cap .k{color:var(--bp-ink-3)}.cap .bar{letter-spacing:-.03em;overflow:hidden;white-space:nowrap}.cap .v{text-align:right;font-variant-numeric:tabular-nums}
.manifest{display:grid;grid-template-columns:auto minmax(0,1fr);gap:.4rem 2.5ch;font-size:.78rem;align-items:baseline}.manifest dt{color:var(--bp-ink-3);white-space:nowrap}.manifest dd{margin:0;color:var(--bp-ink);min-width:0;overflow-wrap:anywhere}.manifest dd small{display:block;color:var(--bp-ink-3);font-size:.7rem;margin-top:.1rem}.copyblock{border:1px solid var(--bp-rule);border-left:3px solid var(--bp-second);padding:.8rem 1.5ch;font-size:.74rem;line-height:1.5;overflow-x:auto;white-space:pre-wrap;margin:0;color:var(--bp-ink)}.copyblock b{color:var(--bp-accent);font-weight:400}.copyblock s{color:var(--bp-ink-3);text-decoration:none}
.steps{display:grid;gap:0}.step{border-bottom:1px dotted var(--bp-rule);display:grid;grid-template-columns:5ch minmax(0,1fr) auto;gap:2ch;align-items:baseline;padding:.55rem 0}.step:last-child{border-bottom:none}.step__n{font-family:'Silkscreen',monospace;font-size:.64rem;color:var(--bp-accent);letter-spacing:.1em}.step__t{font-size:.82rem}.step__t small{display:block;color:var(--bp-ink-3);font-size:.7rem;margin-top:.15rem}.step__s{font-size:.7rem;text-transform:uppercase;letter-spacing:.06em;white-space:nowrap}.step[data-state="done"] .step__s{color:var(--bp-ok)}.step[data-state="now"] .step__s{color:var(--bp-second)}.step[data-state="wait"] .step__s{color:var(--bp-ink-3)}
.contract{display:grid;gap:0}.cx{display:grid;grid-template-columns:16ch minmax(0,1fr) auto;gap:2ch;align-items:baseline;padding:.45rem 0;border-bottom:1px dotted var(--bp-rule)}.cx:last-child{border-bottom:none}.cx .k{color:var(--bp-ink-3);font-size:.76rem}.cx .v{font-size:.8rem;overflow-wrap:anywhere;color:var(--bp-ink)}.cx .v small{display:block;color:var(--bp-ink-3);font-size:.7rem;margin-top:.15rem}.cx .c{justify-self:end}
.usage{display:grid;grid-template-columns:13ch minmax(0,1fr) 14ch;gap:.6rem 2ch;align-items:baseline;font-size:.78rem}.usage .k{color:var(--bp-ink-3)}.usage .g{letter-spacing:-.04em;overflow:hidden;white-space:nowrap;color:var(--bp-accent)}.usage .v{text-align:right;font-variant-numeric:tabular-nums}
.feed{display:grid;gap:.2rem;font-size:.76rem}.feed div{display:flex;gap:2ch;align-items:baseline;border-bottom:1px dotted var(--bp-rule);padding:.25rem 0}.feed time{color:var(--bp-ink-3);flex:none;font-variant-numeric:tabular-nums}.feed b{color:var(--bp-second);font-weight:500}
.ranges{display:flex;gap:.2rem;align-items:center;flex-wrap:wrap}.bp .ranges a{font-family:'Silkscreen',monospace;font-size:.62rem;letter-spacing:.08em;text-decoration:none;color:var(--bp-ink-3);padding:.35rem 1.2ch;border:1px solid transparent}.bp .ranges a:hover{color:var(--bp-accent)}.bp .ranges a[aria-current="true"]{color:var(--bp-paper);background:var(--bp-accent);border-color:var(--bp-accent)}
.analytics-chart{height:11rem;display:flex;align-items:stretch;gap:min(.45ch,.35vw);border-bottom:1px solid var(--bp-rule);padding:.5rem 0 0}.analytics-chart>span{flex:1;min-width:.25ch;display:flex;align-items:flex-end}.analytics-chart i{display:block;width:100%;height:var(--v);background:var(--bp-accent);min-height:.3rem}.analytics-chart>span:nth-child(2n) i{background:var(--bp-second)}.chart-axis{display:flex;justify-content:space-between;color:var(--bp-ink-3);font-size:.66rem;margin-top:.35rem}.analytics-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:2rem 3ch}.breakdown{display:grid;gap:.35rem}.breakdown>div{display:grid;grid-template-columns:minmax(10ch,1fr) 19ch 7ch;gap:1.5ch;align-items:baseline;font-size:.74rem;border-bottom:1px dotted var(--bp-rule);padding:.25rem 0}.breakdown .label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.breakdown .meter{color:var(--bp-second);letter-spacing:-.06em;white-space:nowrap;overflow:hidden}.breakdown b{text-align:right;font-variant-numeric:tabular-nums}
.bp-table tbody tr[data-href]{cursor:pointer}.bp-table tbody tr[data-href] td:first-child::after{content:" ▸";color:var(--bp-rule)}.bp-table tbody tr[data-href]:hover td:first-child::after{color:var(--bp-accent)}
.token-box{border:1px solid var(--bp-ok);border-left:4px solid var(--bp-ok);padding:1rem 2ch;display:grid;gap:.5rem;background:color-mix(in srgb,var(--bp-ok) 6%,var(--bp-paper));overflow-wrap:anywhere}.token-box code{white-space:normal}.notice{border-left:3px solid var(--bp-warn);padding:.7rem 1.5ch;background:color-mix(in srgb,var(--bp-warn) 7%,var(--bp-paper));font-size:.78rem}.danger-panel .bp-fr__t{color:var(--bp-err)}
.bp a.bp-btn{color:var(--bp-ink);text-decoration:none}.bp a.bp-btn--primary{color:var(--bp-paper)}
.bp-input textarea{font:inherit;flex:1;min-width:0;border:none;background:transparent;color:var(--bp-ink);padding:.25rem 1ch;resize:vertical}.bp-input textarea:focus{outline:none}
.auth-dialog{border:1px solid var(--bp-ink);background:var(--bp-paper);color:var(--bp-ink);padding:0;width:min(64ch,92vw);box-shadow:6px 6px 0 var(--bp-shadow);font:inherit;margin-top:12vh}.auth-dialog::backdrop,dialog.confirm::backdrop{background:rgba(20,16,25,.4)}.auth-in{padding:1.5rem 2.5ch;display:grid;gap:1rem}.auth-in h1{font-size:1.4rem}.recovery-help{margin-top:.5rem;border-top:1px dotted var(--bp-rule);padding-top:.8rem}dialog.confirm{border:1px solid var(--bp-err);background:var(--bp-paper);color:var(--bp-ink);padding:0;width:min(64ch,92vw);box-shadow:6px 6px 0 var(--bp-shadow);font:inherit;margin-top:16vh}dialog.confirm .in{padding:1.3rem 2.5ch;display:grid;gap:1rem}dialog.confirm h3{color:var(--bp-err)}
.toast{position:fixed;left:50%;bottom:1.6rem;transform:translate(-50%,8rem);background:var(--bp-ink);color:var(--bp-paper);padding:.55rem 2ch;z-index:80;font-size:.78rem;transition:transform .15s}.toast[data-on="1"]{transform:translate(-50%,0)}
.bp-panel.bp-framed{border:none;padding:0}.bp-panel.bp-framed[data-title]::before{content:none}.bp-fr{display:flex;color:var(--bp-rule);white-space:nowrap;line-height:1;user-select:none;font-size:1rem}.bp-fr i{font-style:normal}.bp-fr__h{flex:1 1 0;min-width:0;overflow:hidden}.bp-fr__t{font-family:'Silkscreen',monospace;font-size:.66rem;letter-spacing:.12em;color:var(--bp-accent);padding:0 .6ch;position:relative;top:-.06em}.bp-fr__mid{position:relative;padding:0 1ch}.bp-fr__v{position:absolute;top:0;bottom:0;width:1ch;overflow:hidden;word-break:break-all;color:var(--bp-rule);line-height:1;font-size:1rem;pointer-events:none}.bp-fr__body{min-width:0;padding:1.1rem 1ch 1.2rem}
@media(max-width:900px){.orb{grid-template-columns:minmax(0,1fr)}.rail{position:static;height:auto;border-right:none;border-bottom:1px solid var(--bp-rule)}.rail .navlist{grid-template-columns:repeat(auto-fit,minmax(20ch,1fr))}.hd{position:static}.main{padding:1.4rem 2ch 5rem}}
@media(max-width:700px){.kinds,.sizes,.analytics-grid{grid-template-columns:1fr}.kind,.size{border-left:none;border-top:1px dotted var(--bp-rule)}.kind:first-child,.size:first-child{border-top:none}.cx{grid-template-columns:minmax(0,1fr) auto}.cx .k{grid-column:1/-1}.usage,.cap{grid-template-columns:10ch minmax(0,1fr)}.usage .v,.cap .v{grid-column:2}.sl{grid-template-columns:9ch minmax(0,1fr)}.sl .v{grid-column:2}.step{grid-template-columns:4ch minmax(0,1fr)}.step__s{grid-column:2}.breakdown>div{grid-template-columns:minmax(8ch,1fr) 12ch 5ch}.hd #live{display:none}}


/* ═══ §1 · TOKENS ══════════════════════════════════════════════════════════
   Fifteen values are the entire palette. Nothing below hard-codes a colour.
   The ground is white on purpose: this system is a printout, and a printout
   has no night version. If you need dark mode, redefine these fifteen — the
   component layer does not change. */
:root{
  --bp-paper:#FFFFFF;      /* the ground. never moves.                      */
  --bp-paper-2:#F6F5F9;    /* raised: table heads, hover, active card       */
  --bp-paper-3:#EDEBF3;    /* sunk: chips, inset blocks                     */
  --bp-rule:#C7C2D5;       /* every rail, border and box-drawing character  */

  --bp-ink:#141019;        /* body text and headings                        */
  --bp-ink-2:#544E68;      /* secondary prose                               */
  --bp-ink-3:#8B849F;      /* labels, hints, disabled                       */

  --bp-accent:#D4006E;     /* identity: wordmark, the one filled button     */
  --bp-accent-soft:#FFE7F3;
  --bp-second:#1F35C4;     /* info, links, second data series, duotone      */
  --bp-second-soft:#E3E6FA;

  --bp-ok:#1B6B36;
  --bp-warn:#8A5300;
  --bp-err:#BE1E1E;        /* errors and destructive ONLY. never decoration */
  --bp-shadow:#DCD8E6;     /* the 3px hard drop shadow                      */

  color-scheme:light;
}


/* ═══ §2 · PALETTES ════════════════════════════════════════════════════════
   <html data-palette="blueprint">. Each moves the primary, the secondary, the
   neutral bias and the semantics together — those four are one decision. */

[data-palette="riso"]{        /* orange + teal, warm greys, no magenta      */
  --bp-paper-2:#F8F5F2; --bp-paper-3:#F0EBE5; --bp-rule:#CFC6BC;
  --bp-ink:#1A1512; --bp-ink-2:#5C5248; --bp-ink-3:#948878;
  --bp-accent:#D64000; --bp-accent-soft:#FFE8DC;
  --bp-second:#006E7A; --bp-second-soft:#DAEFF2;
  --bp-ok:#2A6A20; --bp-warn:#8A5300; --bp-err:#B5122A; --bp-shadow:#E4DCD2;
}
[data-palette="spot"]{        /* black + one orange. hierarchy by weight    */
  --bp-paper-2:#F5F5F5; --bp-paper-3:#EBEBEB; --bp-rule:#C4C4C4;
  --bp-ink:#0D0D10; --bp-ink-2:#4D4D52; --bp-ink-3:#86868C;
  --bp-accent:#C74400; --bp-accent-soft:#FFE6DA;
  --bp-second:#0D0D10; --bp-second-soft:#EBEBEB;
  --bp-ok:#3A3A40; --bp-warn:#3A3A40; --bp-err:#C74400; --bp-shadow:#DCDCDC;
}
[data-palette="teletext"]{    /* blue + magenta, saturated semantics        */
  --bp-paper-2:#F4F5FA; --bp-paper-3:#E9EBF5; --bp-rule:#BEC3DA;
  --bp-ink:#0B0E1A; --bp-ink-2:#464C66; --bp-ink-3:#7C8399;
  --bp-accent:#0026CA; --bp-accent-soft:#DDE2FB;
  --bp-second:#C4008C; --bp-second-soft:#FCE0F4;
  --bp-ok:#0A7A2F; --bp-warn:#8F5D00; --bp-err:#C4101C; --bp-shadow:#D8DCEC;
}
[data-palette="acid"]{        /* violet + lime; lime doubles as success     */
  --bp-paper-2:#F4F6F5; --bp-paper-3:#E8EDEA; --bp-rule:#BFC9C4;
  --bp-ink:#101418; --bp-ink-2:#48555C; --bp-ink-3:#7E8C92;
  --bp-accent:#5B21C7; --bp-accent-soft:#EBE2FB;
  --bp-second:#4F7A00; --bp-second-soft:#EAF3D6;
  --bp-ok:#4F7A00; --bp-warn:#8A6000; --bp-err:#C01F3C; --bp-shadow:#DDE4E0;
}
[data-palette="blueprint"]{   /* prussian blue + rust, drafting greys       */
  --bp-paper-2:#F3F5F8; --bp-paper-3:#E7ECF2; --bp-rule:#B9C4D2;
  --bp-ink:#0F1A2B; --bp-ink-2:#44556B; --bp-ink-3:#7A8A9E;
  --bp-accent:#123A8C; --bp-accent-soft:#DCE5F5;
  --bp-second:#A34A00; --bp-second-soft:#F7E4D4;
  --bp-ok:#1C6B4A; --bp-warn:#8A5A00; --bp-err:#A81B22; --bp-shadow:#D7DFE9;
}
[data-palette="oxide"]{       /* oxblood + brass, ledger paper             */
  --bp-paper-2:#F7F5F3; --bp-paper-3:#EEEAE6; --bp-rule:#C8C0B8;
  --bp-ink:#191512; --bp-ink-2:#57504A; --bp-ink-3:#8E857C;
  --bp-accent:#8C1B3F; --bp-accent-soft:#F8E3E9;
  --bp-second:#6E6100; --bp-second-soft:#F2EED6;
  --bp-ok:#3D6B1E; --bp-warn:#8A5300; --bp-err:#B1121C; --bp-shadow:#E2DCD5;
}
[data-palette="plotter"]{     /* pen green + magenta, two pens             */
  --bp-paper-2:#F4F6F6; --bp-paper-3:#E8EDEC; --bp-rule:#BCC6C6;
  --bp-ink:#0E1614; --bp-ink-2:#42514E; --bp-ink-3:#798785;
  --bp-accent:#0B6E4F; --bp-accent-soft:#DCF0E8;
  --bp-second:#B3007A; --bp-second-soft:#FBDFF1;
  --bp-ok:#0F6C74; --bp-warn:#8A5A00; --bp-err:#BC1A28; --bp-shadow:#DAE2E1;
}
[data-palette="nightshift"]{  /* ink leads; coral is pure state           */
  --bp-paper-2:#F5F5F7; --bp-paper-3:#EAEAEE; --bp-rule:#C3C3CB;
  --bp-ink:#16161A; --bp-ink-2:#4B4B55; --bp-ink-3:#85858F;
  --bp-accent:#16161A; --bp-accent-soft:#EAEAEE;
  --bp-second:#E0004D; --bp-second-soft:#FFE0EA;
  --bp-ok:#1F6B3C; --bp-warn:#84550A; --bp-err:#C4102C; --bp-shadow:#DCDCE2;
}


/* ═══ §3 · BASE ════════════════════════════════════════════════════════════
   <body class="bp"> — or wrap only the subtree you want the system to own,
   so this can live beside an existing stylesheet without a fight.

   Three faces, three jobs:
     Martian Mono  display — wide and engineered. Keeps it from reading as
                   nostalgia cosplay. Headings only.
     JetBrains Mono body, data, and every box-drawing character.
     Silkscreen    bitmap. Rationed: labels, column heads, frame titles.
                   Never a sentence. */
.bp{
  background:var(--bp-paper);
  color:var(--bp-ink);
  font-family:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:14px;
  line-height:1.5;
  font-variant-ligatures:none;
}
.bp h1,.bp h2,.bp h3{
  font-family:'Martian Mono','JetBrains Mono',monospace;
  font-weight:800; text-wrap:balance; margin:0;
}
.bp h1{font-size:1.6rem; letter-spacing:-.04em}
.bp h2{font-size:1rem; letter-spacing:-.03em; text-transform:uppercase}
.bp h3{font-size:.86rem; font-weight:700}
.bp p{margin:0; max-width:min(66ch,100%); color:var(--bp-ink-2)}
.bp p strong{color:var(--bp-ink); font-weight:700}
.bp a{color:var(--bp-second); text-underline-offset:3px}
.bp code{color:var(--bp-accent); white-space:nowrap}
.bp :focus-visible{outline:2px solid var(--bp-accent); outline-offset:2px}

/* <span class="bp-label">LAST SEEN</span> — the bitmap face, used sparingly */
.bp-label{
  font-family:'Silkscreen',monospace; font-size:.62rem;
  text-transform:uppercase; letter-spacing:.14em; color:var(--bp-ink-3);
}
.bp-hint{font-size:.72rem; color:var(--bp-ink-3)}
.bp-hint--err{color:var(--bp-err)}
.bp-num{font-variant-numeric:tabular-nums}   /* anywhere digits line up */
.bp-sr{position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap}

@media (prefers-reduced-motion:reduce){
  .bp *{animation:none!important; transition:none!important}
}


/* ═══ §4 · LAYOUT ══════════════════════════════════════════════════════════
   Everything measures in character cells. Gutter is 2ch everywhere.
     <div class="bp-row">      horizontal, wraps, 2ch gap
     <div class="bp-stack">    vertical, 1rem gap
     <div class="bp-grid">     auto-fit columns, min 24ch each
   minmax(0,1fr) on every track: without it a long unbreakable run (a rail,
   a wide <pre>) sizes the track to its own max-content and blows the page
   sideways. This is the single most common way a monospace layout breaks. */
.bp-row{display:flex; gap:2ch; flex-wrap:wrap; align-items:center}
.bp-stack{display:grid; grid-template-columns:minmax(0,1fr); gap:1rem}
.bp-grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(24ch,1fr)); gap:2ch}


/* ═══ §5 · PANEL ═══════════════════════════════════════════════════════════
   <section class="bp-panel" data-title="01 · FOUNDATIONS"> … </section>

   A ruled box with the title notched into the top edge — the TUI window.
   This is the no-JS version: the sides are 1px rules. Load the appendix
   script (§12) to replace them with real ┌ ─ ┐ │ └ ┘ characters that reflow
   at any width and draw themselves in on scroll. Everything else is
   identical either way. */
.bp-panel{
  position:relative;
  border:1px solid var(--bp-rule);
  padding:1.6rem 2ch 1.4rem;
  margin:0 0 2.6rem;
}
.bp-panel[data-title]::before{
  content:attr(data-title);
  position:absolute; top:0; left:2ch; transform:translateY(-50%);
  background:var(--bp-paper); padding:0 1ch;
  font-family:'Silkscreen',monospace; font-size:.66rem; letter-spacing:.12em;
  color:var(--bp-accent); white-space:nowrap;
}
.bp-panel--tight{padding:.9rem 1.5ch}


/* ═══ §6 · BUTTONS ═════════════════════════════════════════════════════════
   <button class="bp-btn">Preview diff</button>
   <button class="bp-btn bp-btn--primary">▶ Deploy</button>
   <button class="bp-btn bp-btn--danger">✕ Roll back</button>
   <button class="bp-btn bp-btn--ghost">Cancel</button>

   The brackets are typed, not drawn. Exactly one primary per screen: it is
   the only raised object, and it presses into its own shadow. */
.bp-btn{
  font:inherit; cursor:pointer; padding:.22rem .6ch;
  background:transparent; border:none; color:var(--bp-ink);
  transition:color .12s;
}
.bp-btn::before{content:"[ "; color:var(--bp-rule)}
.bp-btn::after{content:" ]"; color:var(--bp-rule)}
.bp-btn:hover,
.bp-btn:hover::before,
.bp-btn:hover::after{color:var(--bp-accent)}

.bp-btn--primary{
  background:var(--bp-accent); color:var(--bp-paper); font-weight:700;
  box-shadow:3px 3px 0 var(--bp-shadow);
  transition:transform .07s, box-shadow .07s;
}
.bp-btn--primary::before,
.bp-btn--primary::after{color:rgba(255,255,255,.55)}
.bp-btn--primary:hover{background:var(--bp-ink); color:var(--bp-paper)}
.bp-btn--primary:active{transform:translate(3px,3px); box-shadow:0 0 0 var(--bp-shadow)}

.bp-btn--danger{color:var(--bp-err)}
.bp-btn--danger:hover{background:var(--bp-err); color:var(--bp-paper)}
.bp-btn--danger:hover::before,
.bp-btn--danger:hover::after{color:rgba(255,255,255,.55)}

.bp-btn--ghost{color:var(--bp-ink-3)}
.bp-btn--ghost:hover{color:var(--bp-accent)}

/* pointer-events:none, not just opacity — otherwise a disabled variant still
   picks up its own :hover fill and paints faded text on a faded ground. */
.bp-btn[disabled]{opacity:.4; cursor:not-allowed; pointer-events:none}


/* ═══ §7 · FIELDS ══════════════════════════════════════════════════════════
   <label class="bp-field">
     <span class="bp-label">Cluster</span>
     <span class="bp-input"><b>[</b><input value="prod-eu-3"><b>]</b></span>
     <span class="bp-hint">Tab-completes from your last 20 targets.</span>
   </label>

   <label class="bp-check"><input type="checkbox"><span class="bp-mark"></span>Drain first</label>
   <label class="bp-check"><input type="radio" name="n"><span class="bp-mark"></span>Notify on failure</label>

   Real inputs underneath, always — the ASCII is presentation, never
   structure. Focus, tabbing and screen readers keep working. */
.bp-field{display:grid; gap:.3rem}
.bp-input{display:flex; align-items:center; border-bottom:1px dotted var(--bp-rule)}
.bp-input > b{color:var(--bp-rule); font-weight:400; transition:color .12s}
.bp-input:focus-within{border-bottom-color:var(--bp-accent)}
.bp-input:focus-within > b{color:var(--bp-accent)}
.bp-input input,
.bp-input select{
  font:inherit; flex:1; min-width:0; border:none; background:transparent;
  color:var(--bp-ink); padding:.25rem 1ch; appearance:none;
}
.bp-input input:focus,
.bp-input select:focus{outline:none}
.bp-input input::placeholder{color:var(--bp-ink-3)}
.bp-input--err{border-bottom-color:var(--bp-err)}
.bp-input--err > b{color:var(--bp-err)}

.bp-check{display:flex; gap:1ch; align-items:baseline; cursor:pointer; user-select:none; color:var(--bp-ink-2)}
.bp-check input{position:absolute; opacity:0; width:0; height:0}
.bp-check .bp-mark{color:var(--bp-rule); flex:none; letter-spacing:-.02em}
.bp-check input[type="checkbox"] + .bp-mark::after{content:"[ ]"}
.bp-check input[type="checkbox"]:checked + .bp-mark::after{content:"[×]"}
.bp-check input[type="radio"] + .bp-mark::after{content:"( )"}
.bp-check input[type="radio"]:checked + .bp-mark::after{content:"(•)"}
.bp-check input:checked + .bp-mark{color:var(--bp-accent)}
.bp-check input:focus-visible + .bp-mark{outline:2px solid var(--bp-accent); outline-offset:2px}
.bp-check:hover{color:var(--bp-ink)}


/* ═══ §8 · STATUS ══════════════════════════════════════════════════════════
   <span class="bp-pill bp-pill--ok">✓ healthy</span>
   <span class="bp-pill bp-pill--warn">▲ degraded</span>
   <span class="bp-pill bp-pill--err">✕ down</span>
   <span class="bp-pill bp-pill--idle">◦ draining</span>
   <span class="bp-pill bp-pill--info">◐ deploying</span>

   Glyph + word + colour, every time. Drop any one and the row still reads —
   in a screenshot, on a mono terminal, or to anyone who does not separate
   red from green. */
.bp-pill{
  display:inline-flex; gap:.7ch; align-items:baseline;
  font-size:.74rem; text-transform:uppercase; letter-spacing:.06em;
}
.bp-pill::before{content:"‹"; color:var(--bp-rule)}
.bp-pill::after{content:"›"; color:var(--bp-rule)}
.bp-pill--ok{color:var(--bp-ok)}
.bp-pill--warn{color:var(--bp-warn)}
.bp-pill--err{color:var(--bp-err)}
.bp-pill--idle{color:var(--bp-ink-3)}
.bp-pill--info{color:var(--bp-second)}


/* ═══ §9 · TABLE ═══════════════════════════════════════════════════════════
   <div class="bp-tablewrap"><table class="bp-table"> … </table></div>

   The one place hairlines beat characters: a │ inside every cell fights the
   column padding and falls out of step the moment a value gets wider. The
   header rule is a CSS double border — the ═ of CP437, drawn in a way that
   can never drift from the columns. Always wrap: wide content scrolls in its
   own container so the page body never scrolls sideways. */
.bp-tablewrap{overflow-x:auto}
.bp-table{border-collapse:collapse; width:100%; font-size:.8rem; font-variant-numeric:tabular-nums}
.bp-table th{
  text-align:left; padding:.35rem 1.5ch; white-space:nowrap;
  font-family:'Silkscreen',monospace; font-weight:400; font-size:.64rem; letter-spacing:.1em;
  color:var(--bp-ink-2); border-bottom:3px double var(--bp-ink-3);
}
.bp-table td{padding:.3rem 1.5ch; border-bottom:1px dotted var(--bp-rule)}
.bp-table th + th,
.bp-table td + td{border-left:1px solid var(--bp-rule)}
.bp-table tbody tr:last-child td{border-bottom:none}
.bp-table tbody tr{transition:background .12s}
.bp-table tbody tr:hover{background:var(--bp-accent-soft)}
.bp-table td.bp-num{text-align:right}


/* ═══ §10 · DATA ═══════════════════════════════════════════════════════════
   <pre class="bp-art">…</pre>  and inside it:
     <b> accent   <i> secondary   <u> ok   <s> muted

   Twelve characters are the whole vocabulary:
     shading   ░ ▒ ▓ █          density, heatmaps, spoilers
     eighths   ▁▂▃▄▅▆▇█         bars and sparklines
   No SVG, no chart library. It copies into a terminal or a commit message
   and still means the same thing. Always ship a legend with a shading ramp.

     p99 latency  ▁▂▂▃▂▄▆█▅▃▂▂▁  318ms ▲
     load         ███████░░░      70%
     mon  ███████▓▓▓▓▓▒▒▒▓▓▓▓█████
     ░ <40%   ▒ 40–70%   ▓ 70–90%   █ >90% */
.bp-art{
  margin:0; overflow-x:auto; white-space:pre;
  font-size:.78rem; line-height:1.35; color:var(--bp-ink-2);
}
.bp-art b{color:var(--bp-accent); font-weight:400}
.bp-art i{color:var(--bp-second); font-style:normal}
.bp-art u{color:var(--bp-ok); text-decoration:none}
.bp-art s{color:var(--bp-ink-3); text-decoration:none}

/* duotone ramp from a single token — three weights, no extra colours */
.bp-s2{color:var(--bp-second)}
.bp-s2-mid{color:color-mix(in srgb, var(--bp-second) 55%, var(--bp-paper))}
.bp-s2-low{color:color-mix(in srgb, var(--bp-second) 28%, var(--bp-paper))}


/* ═══ §11 · RECALL ═════════════════════════════════════════════════════════
   <div class="bp-card">
     <p class="bp-card__q">Question…</p>
     <div class="bp-card__a">
       <p class="bp-card__at" aria-hidden="true">Answer…</p>
       <button class="bp-spoil" aria-expanded="false" aria-label="Reveal the answer"></button>
     </div>
     <div class="bp-row">
       <button class="bp-btn bp-btn--ok" disabled>✓ got it</button>
       <button class="bp-btn bp-btn--miss" disabled>✕ missed</button>
     </div>
   </div>

   The spoiler is a real block of █ sized to the answer box and filled by the
   appendix script. Without that script it stays a solid fill and still hides
   the answer — it just will not erode. line-height:1 matters: at anything larger the block
   characters leave seams and the answer leaks through before you reveal it.
   Keep the answer aria-hidden until it is actually revealed. */
.bp-card{display:grid; gap:1.1rem}
.bp-card__q{font-size:.95rem; color:var(--bp-ink); min-height:4.4em; white-space:pre-wrap; max-width:none}
.bp-card__a{position:relative; min-height:5em}
.bp-card__at{font-size:.95rem; color:var(--bp-second); white-space:pre-wrap; user-select:none; max-width:none}
.bp-card__a[data-open="1"] .bp-card__at{user-select:auto}
.bp-spoil{
  all:unset; position:absolute; inset:0; display:block; cursor:pointer; overflow:hidden;
  font-family:inherit; font-size:1rem; line-height:1; white-space:pre; color:var(--bp-accent);
  background:var(--bp-accent);   /* solid fallback: covers the answer even with no JS */
}
.bp-spoil:hover{color:color-mix(in srgb, var(--bp-accent) 74%, var(--bp-paper))}
.bp-spoil:focus-visible{outline:2px solid var(--bp-ink); outline-offset:2px}
.bp-btn--ok{color:var(--bp-ok)}
.bp-btn--ok:hover{background:var(--bp-ok); color:var(--bp-paper)}
.bp-btn--ok:hover::before,.bp-btn--ok:hover::after{color:rgba(255,255,255,.55)}
.bp-btn--miss{color:var(--bp-err)}
.bp-btn--miss:hover{background:var(--bp-err); color:var(--bp-paper)}
.bp-btn--miss:hover::before,.bp-btn--miss:hover::after{color:rgba(255,255,255,.55)}


/* ═══════════════════════════════════════════════════════════════════════════
   §12 · APPENDIX — optional JavaScript
   ───────────────────────────────────────────────────────────────────────────
   Everything above works without this. Copy the block below into a .js file
   only if you want the two things CSS genuinely cannot do: rails made of real
   box-drawing characters, and the erosion animations.

   ── A · REAL CHARACTER FRAMES ──────────────────────────────────────────────
   Turns <section class="bp-panel" data-title="…"> into ┌─┤ TITLE ├──┐ with
   │ down both sides. Horizontal rails are a long run of ─ clipped by
   overflow. Vertical rails are a run of │ in a 1ch column, word-break
   forcing one glyph per line, absolutely positioned so their 700 lines never
   contribute height. Reflows at any width, no resize handler, no measuring.

   Add this CSS alongside it:

     .bp-panel.bp-framed{border:none; padding:0}
     .bp-fr{display:flex; color:var(--bp-rule); white-space:nowrap;
            line-height:1; user-select:none; font-size:1rem}
     .bp-fr__h{flex:1 1 0; min-width:0; overflow:hidden}
     .bp-fr__t{font-family:'Silkscreen',monospace; font-size:.66rem;
               letter-spacing:.12em; color:var(--bp-accent); padding:0 .6ch}
     .bp-fr__mid{position:relative; padding:0 1ch}
     .bp-fr__v{position:absolute; top:0; bottom:0; width:1ch; overflow:hidden;
               word-break:break-all; color:var(--bp-rule); line-height:1;
               font-size:1rem; pointer-events:none}
     .bp-fr__body{min-width:0; padding:1.1rem 1ch 1.2rem}

   And this JS:

     const H = '─'.repeat(700), V = '│'.repeat(700);
     const rail = side => Object.assign(document.createElement('span'), {
       className: 'bp-fr__v', textContent: V, ariaHidden: 'true'
     });
     document.querySelectorAll('.bp-panel').forEach(el => {
       const t = el.dataset.title;
       const top = document.createElement('div');
       top.className = 'bp-fr';
       top.innerHTML = t
         ? `<i>┌─┤</i><b class="bp-fr__t"></b><i>├</i><span class="bp-fr__h">${H}</span><i>┐</i>`
         : `<i>┌</i><span class="bp-fr__h">${H}</span><i>┐</i>`;
       if (t) top.querySelector('.bp-fr__t').textContent = t;
       const bot = document.createElement('div');
       bot.className = 'bp-fr';
       bot.innerHTML = `<i>└</i><span class="bp-fr__h">${H}</span><i>┘</i>`;
       const mid = document.createElement('div'); mid.className = 'bp-fr__mid';
       const body = document.createElement('div'); body.className = 'bp-fr__body';
       while (el.firstChild) body.appendChild(el.firstChild);
       const l = rail(); l.style.left = 0;
       const r = rail(); r.style.right = 0;
       mid.append(l, body, r);
       el.append(top, mid, bot);
       el.classList.add('bp-framed');
     });

   Two traps worth knowing, both of which cost real debugging time:
     · A vertical rail left in normal flow makes its container 700 lines tall.
       It has to be absolutely positioned; overflow:hidden does not cap height.
     · Never position a rail with a magic offset like left:26ch. `ch` resolves
       against the element's own font-size, so a rail at 1rem and a sidebar at
       .78rem disagree and the rail lands in the middle of the content. Put it
       in a stretching flex slot and let layout place it.

   ── B · SPOILER EROSION ────────────────────────────────────────────────────
   Each cell walks down the ramp on its own schedule, so the block breaks up
   instead of wiping. Fill first, then dissolve on click.

     const RAMP = [' ', '░', '▒', '▓', '█'];

     function fill(spoil){                       // size the block to its box
       const probe = document.createElement('span');
       probe.style.cssText = 'position:absolute;visibility:hidden;white-space:pre';
       probe.textContent = '0'.repeat(50);
       spoil.appendChild(probe);
       const w = probe.getBoundingClientRect().width / 50 || 9;
       probe.remove();
       const lh = parseFloat(getComputedStyle(spoil).lineHeight) || 16;
       const cols = Math.ceil(spoil.clientWidth / w);
       const rows = Math.ceil(spoil.clientHeight / lh);
       spoil.dataset.cols = cols; spoil.dataset.rows = rows;
       spoil._seed = Array.from({length: cols * rows}, Math.random);
       spoil.textContent = Array.from({length: rows}, () => '█'.repeat(cols)).join('\n');
     }

     function dissolve(spoil, done, dur = 680){
       const cols = +spoil.dataset.cols, rows = +spoil.dataset.rows, t0 = performance.now();
       (function step(now){
         const p = Math.min(1, (now - t0) / dur);
         let out = '';
         for (let y = 0; y < rows; y++){
           for (let x = 0; x < cols; x++){
             const local = Math.max(0, Math.min(1, (p - spoil._seed[y * cols + x] * 0.55) / 0.45));
             out += RAMP[4 - Math.round(local * 4)];
           }
           out += '\n';
         }
         spoil.textContent = out;
         p < 1 ? requestAnimationFrame(step) : (spoil.hidden = true, done && done());
       })(t0);
     }

   ── C · TEXT SHRED ─────────────────────────────────────────────────────────
   Swaps card text out and the next one in. Spaces and newlines are never
   replaced — touch them and the line wrapping jumps while it animates.

     const GRIT = '░▒▓█╬╦╣═║┼+*·';

     function scramble(el, text, dir, dur, done){   // dir: 'out' | 'in'
       const seed = Array.from(text, Math.random), t0 = performance.now();
       (function step(now){
         const p = Math.min(1, (now - t0) / dur);
         let out = '';
         for (let i = 0; i < text.length; i++){
           const c = text[i];
           if (c === ' ' || c === '\n') { out += c; continue; }
           const local = Math.max(0, Math.min(1, (p - seed[i] * 0.5) / 0.5));
           const gone = dir === 'out' ? local : 1 - local;
           out += gone > 0.72 ? ' '
                : gone > 0.02 ? GRIT[(Math.random() * GRIT.length) | 0]
                : c;
         }
         el.textContent = out;
         p < 1 ? requestAnimationFrame(step)
               : (el.textContent = dir === 'out' ? text.replace(/\S/g, ' ') : text, done && done());
       })(t0);
     }

   Wrap all three in a prefers-reduced-motion check and jump to the end state:

     const STILL = matchMedia('(prefers-reduced-motion: reduce)').matches;

   ═══════════════════════════════════════════════════════════════════════════ */
