/* heros.css — the token system and the furniture every page shares.
 *
 * 🔴 ONE copy of the palette. The console carried its own `:root` block, transcribed from
 * heros-agent.space; adding three more pages each with their own copy is four places for a colour to
 * drift and no way to tell which one is right. The console now links this file too, and its inline
 * block is gone.
 *
 * The system was dark-committed. It is now dark-DEFAULT with a real light theme beside it, and every
 * surface still paints its own background rather than borrowing the host's.
 *
 * ── how the two themes are resolved ────────────────────────────────────────────────────────────────
 * Dark is the base `:root`. Light is stated twice, and deliberately:
 *
 *   :root[data-theme="light"]                          — somebody chose light in the switch
 *   @media (prefers-color-scheme:light){ :root:not([data-theme="dark"]) }
 *                                                      — nobody chose anything; their device did
 *
 * 🔴 The alternative — having heros-theme.js write the RESOLVED theme onto <html> — would let this
 * be written once, and it is the wrong trade. It makes JavaScript load-bearing for a default, it
 * freezes whichever answer was true at load if the OS flips at dusk, and it repaints after first
 * paint, which is the flash. CSS answers the OS; the attribute is only for an override. The price is
 * one duplicated block, and `TestTheTwoLightPalettesAgree` in cmd/herosd fails the build if the two
 * copies ever disagree — see workflow/CI note in that test.
 *
 * ── why the washes are channels, not colours ───────────────────────────────────────────────────────
 * 🔴 Hairlines and tints used to be written `#ffffff0d` — a colour with alpha digits appended. That
 * form cannot be themed: `var(--x)0d` is not a colour, it is a silently DROPPED declaration, so the
 * border simply vanishes and the page still renders. Surfaces are built from `rgba(var(--fg-rgb),α)`
 * instead, and a theme moves the channel. Status tints use `color-mix(…, transparent)` for the same
 * reason — change `--bad` and every tint of it follows.
 */
:root{
  color-scheme:dark;

  --fg-rgb:255,255,255;

  --canvas:#080d17; --panel:#0a1020; --card:#0e1422; --secondary:#151d2e; --muted:#141b2b;
  --secondary-hi:#1b2540;
  /* The lighter frame the home page's rounded shell sits in, the two sunken blocks inside it, and
     the pit a diff is printed into. */
  --frame:#141a24; --sunk:#0d1626; --sunk-2:#101b2e; --pit:#060a12;

  --ink:#dde2f0; --ink-strong:#ffffff; --ink-muted:#8b95b5; --placeholder:#7c88a8;
  --border:rgba(var(--fg-rgb),.102); --border-strong:rgba(var(--fg-rgb),.161);
  --hairline:rgba(var(--fg-rgb),.051);
  --wash-1:rgba(var(--fg-rgb),.016); --wash-2:rgba(var(--fg-rgb),.024);
  --wash-3:rgba(var(--fg-rgb),.078);
  --grid-line:rgba(var(--fg-rgb),.031); --grid-line-soft:rgba(var(--fg-rgb),.024);
  /* The console's grid is tinted with the accent rather than the foreground; keeping it as a mix
     means it follows the accent into light rather than freezing the dark theme's teal. */
  --grid-line-accent:color-mix(in srgb,var(--accent) 4%,transparent);

  --accent:#2ecfa8; --accent-ink:#051a14; --accent-hi:#48e0bc;
  /* Rules drawn ON the accent band, where neither --ink nor --border is legible. */
  --on-accent-line:rgba(5,38,29,.2); --on-accent-line-soft:rgba(5,38,29,.35);

  --ok:#34d399; --warn:#fbbf24; --bad:#fb7185; --info:#38bdf8; --gated:#a78bfa; --none:#7f8aa8;

  /* The predictive arch — see heros-arc.js. Channels, because the canvas reads them as numbers.
     Dark is green by direction. */
  --arc-dim-rgb:38 190 140; --arc-core-rgb:214 255 236; --arc-alpha:1;

  /* ── depth ──────────────────────────────────────────────────────────────────
     🔴 Elevation is a TOKEN, not a shadow typed at each call site. A shadow is the one thing that
     cannot survive a theme flip unchanged: `rgba(0,0,0,.85)` under a card is a soft lift on black
     and a bruise on paper, and `--edge-top` — the 1px lit rim that makes a panel read as raised —
     is white at 6% on black and white at 90% on paper. Both roles need to move with the theme, and
     both are wrong if only one of them does.
     `--halo` is the page's atmosphere and follows the ARCH, not the accent: green haze under a
     violet arch reads as two unrelated light sources. */
  --lift-1:0 1px 2px rgba(0,0,0,.55);
  --lift-2:0 14px 34px -16px rgba(0,0,0,.75);
  --lift-3:0 48px 100px -46px rgba(0,0,0,.9);
  --edge-top:inset 0 1px 0 rgba(255,255,255,.06);
  --halo:rgba(46,207,168,.14); --halo-soft:rgba(46,207,168,.07);
  --vignette:rgba(0,0,0,.5);

  --radius:.5rem; --pill:9999px;
  --fast:.12s; --base:.18s; --slow:.32s;
  --prose:44rem;
  --display:"Newsreader",Georgia,"Times New Roman",serif;
  --body:"Plus Jakarta Sans",system-ui,-apple-system,"Segoe UI",sans-serif;
  --mono:"JetBrains Mono",SFMono-Regular,Menlo,monospace;
  /* One spacing rhythm, so pages cannot invent their own gaps. */
  --s1:.25rem; --s2:.5rem; --s3:.75rem; --s4:1rem; --s5:1.5rem; --s6:2rem; --s7:3rem; --s8:4.5rem;
}

/* ── light ───────────────────────────────────────────────────────────────────
 * Not the dark palette inverted. Two things genuinely change ROLE rather than value:
 *
 * 🔴 `--frame` is LIGHTER than the canvas in dark and DARKER than it in light. It is the surround the
 *    home page's rounded shell sits on, and a surround only reads as a surround by stepping AWAY from
 *    the page — which is up on black and down on paper.
 * 🔴 `--accent` is not the same teal. `#2ecfa8` on white is 1.8:1; it is a fine fill on black and an
 *    illegible link on paper. Light uses `#0a7d60` (4.5:1 on the canvas, and 4.9:1 for the white text
 *    that sits on top of it when it is a fill), and `--accent-ink` flips from near-black to white to
 *    match. Every status colour is darkened on the same grounds.
 *
 * The arch turns violet here. A "glow" on paper is not light, it is ink density: pale green at 8%
 * alpha on white is not luminous, it is dirt. Violet has somewhere dark to go.
 */
:root[data-theme="light"]{
  color-scheme:light;
  --fg-rgb:15,23,42;
  --canvas:#f7f8fc; --panel:#eef1f8; --card:#ffffff; --secondary:#e9edf6; --muted:#eef1f8;
  --secondary-hi:#dee4f1;
  --frame:#e3e7f2; --sunk:#edf0f8; --sunk-2:#e7ebf5; --pit:#f2f4fa;
  --ink:#111a2e; --ink-strong:#000914; --ink-muted:#59637d; --placeholder:#7b849b;
  --accent:#0a7d60; --accent-ink:#ffffff; --accent-hi:#086a51;
  --on-accent-line:rgba(255,255,255,.3); --on-accent-line-soft:rgba(255,255,255,.5);
  --ok:#0a7a53; --warn:#a05a00; --bad:#be123c; --info:#0a6ea8; --gated:#6d28d9; --none:#5f6880;
  --arc-dim-rgb:139 121 246; --arc-core-rgb:60 24 145; --arc-alpha:.9;
  --lift-1:0 1px 2px rgba(15,23,42,.07);
  --lift-2:0 14px 34px -16px rgba(15,23,42,.16);
  --lift-3:0 48px 100px -46px rgba(15,23,42,.3);
  --edge-top:inset 0 1px 0 rgba(255,255,255,.9);
  --halo:rgba(109,40,217,.1); --halo-soft:rgba(109,40,217,.05);
  --vignette:rgba(15,23,42,.06);
}
@media (prefers-color-scheme:light){
  :root:not([data-theme="dark"]){
    color-scheme:light;
    --fg-rgb:15,23,42;
    --canvas:#f7f8fc; --panel:#eef1f8; --card:#ffffff; --secondary:#e9edf6; --muted:#eef1f8;
    --secondary-hi:#dee4f1;
    --frame:#e3e7f2; --sunk:#edf0f8; --sunk-2:#e7ebf5; --pit:#f2f4fa;
    --ink:#111a2e; --ink-strong:#000914; --ink-muted:#59637d; --placeholder:#7b849b;
    --accent:#0a7d60; --accent-ink:#ffffff; --accent-hi:#086a51;
    --on-accent-line:rgba(255,255,255,.3); --on-accent-line-soft:rgba(255,255,255,.5);
    --ok:#0a7a53; --warn:#a05a00; --bad:#be123c; --info:#0a6ea8; --gated:#6d28d9; --none:#5f6880;
    --arc-dim-rgb:139 121 246; --arc-core-rgb:60 24 145; --arc-alpha:.9;
    --lift-1:0 1px 2px rgba(15,23,42,.07);
    --lift-2:0 14px 34px -16px rgba(15,23,42,.16);
    --lift-3:0 48px 100px -46px rgba(15,23,42,.3);
    --edge-top:inset 0 1px 0 rgba(255,255,255,.9);
    --halo:rgba(109,40,217,.1); --halo-soft:rgba(109,40,217,.05);
    --vignette:rgba(15,23,42,.06);
  }
}

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
  margin:0; background:var(--canvas); color:var(--ink);
  font-family:var(--body); font-size:16px; line-height:1.6;
  /* The faint grid the product's surfaces carry, including the avatar's own background. */
  background-image:
    linear-gradient(to right,var(--grid-line) 1px,transparent 1px),
    linear-gradient(to bottom,var(--grid-line) 1px,transparent 1px);
  background-size:64px 64px;
}
img,svg{max-width:100%; height:auto; display:block}
a{color:var(--accent); text-decoration:none}
a:hover{text-decoration:underline}

/* 🔴 A visible focus ring on every interactive element, and it is never removed. Keyboard users are
   the ones who cannot see where they are without it, and `outline:none` is the single most common way
   a design system becomes unusable for them. */
:where(a,button,input,select,textarea,[tabindex]):focus-visible{
  outline:2px solid var(--accent); outline-offset:2px; border-radius:var(--radius);
}

.wrap{width:min(100% - 2rem, 68rem); margin-inline:auto}
.prose{max-width:var(--prose)}

/* ── nav ─────────────────────────────────────────────────────────────────── */
.nav{
  display:flex; align-items:center; gap:var(--s4);
  padding:var(--s4) 0; border-bottom:1px solid var(--border);
}
.nav .brand{display:flex; align-items:center; gap:var(--s3); font-weight:600; color:var(--ink)}
.nav .brand:hover{text-decoration:none}
.nav .brand img{width:34px; height:34px; border-radius:var(--pill)}
.nav .spacer{flex:1}
.nav-links{display:flex; align-items:center; gap:var(--s2); flex-wrap:wrap}

/* ── buttons ─────────────────────────────────────────────────────────────── */
.btn{
  display:inline-flex; align-items:center; justify-content:center; gap:var(--s2);
  padding:.7rem 1.15rem; border-radius:var(--radius); border:1px solid var(--border-strong);
  background:var(--secondary); color:var(--ink); font:inherit; font-weight:500;
  cursor:pointer; transition:background var(--fast), border-color var(--fast), transform var(--fast);
  /* 44px minimum touch target — a 32px button is a coin toss on a phone. */
  min-height:44px;
}
.btn:hover{background:var(--secondary-hi); text-decoration:none}
.btn:active{transform:translateY(1px)}
.btn-primary{background:var(--accent); color:var(--accent-ink); border-color:transparent; font-weight:600}
.btn-primary:hover{background:var(--accent-hi)}
.btn-quiet{background:transparent; border-color:transparent; color:var(--ink-muted)}
.btn-quiet:hover{background:var(--secondary); color:var(--ink)}
.btn[disabled]{opacity:.55; cursor:not-allowed}

/* ── forms ───────────────────────────────────────────────────────────────── */
.field{
  width:100%; padding:.7rem .85rem; border-radius:var(--radius);
  border:1px solid var(--border-strong); background:var(--panel); color:var(--ink);
  font:inherit; min-height:44px;
}
/* 🔴 A readable placeholder. This was #63709026 — 15% alpha — which is roughly 1.3:1 against the
   field and effectively invisible on a phone in daylight. A placeholder nobody can read is a form
   with no hints, and it fails WCAG 1.4.3 outright. */
.field::placeholder{color:var(--placeholder)}
.field:focus{border-color:var(--accent)}
.label{display:block; font-size:.85rem; color:var(--ink-muted); margin-bottom:var(--s2)}
.hint{font-size:.83rem; color:var(--ink-muted); margin-top:var(--s2)}
.msg{font-size:.88rem; margin-top:var(--s3); min-height:1.3em}
.msg.bad{color:var(--bad)}
.msg.ok{color:var(--ok)}

/* ── password reveal ─────────────────────────────────────────────────────────
 * The button sits INSIDE the field's box, so the field keeps its own focus ring and the control reads
 * as part of it rather than as a second widget beside it. The input gets right padding to match, or
 * long passwords run underneath the button.
 */
.field-wrap{position:relative; display:block}
.field-wrap .field{padding-right:3.1rem}
.reveal{
  position:absolute; right:.35rem; top:50%; transform:translateY(-50%);
  display:grid; place-items:center; width:2.4rem; height:2.4rem;
  background:none; border:0; border-radius:var(--radius); padding:0;
  color:var(--ink-muted); cursor:pointer;
}
.reveal:hover{color:var(--ink); background:var(--secondary)}
.reveal svg{width:20px; height:20px; display:block}
/* 🔴 The button is 2.4rem inside a 44px field rather than a 44px target of its own. Making it 44px
   would overflow the field it sits in. It is a convenience next to a field that is itself a full-size
   target, and it is reachable by keyboard, which is the part that matters most. */

/* ── cards ───────────────────────────────────────────────────────────────── */
.card{
  background:var(--card); border:1px solid var(--border); border-radius:var(--radius);
  padding:var(--s5);
  /* A card is a surface ABOVE the page, and on the two auth pages it is the only object on it — so
     it carries the lit rim and the cast shadow that say so. */
  box-shadow:var(--edge-top), var(--lift-2);
}
.grid{display:grid; gap:var(--s4)}
/* auto-fit + minmax is the whole responsive story for these grids: they reflow at any width without
   a breakpoint, so there is no width at which the layout is merely untested. */
.grid-3{grid-template-columns:repeat(auto-fit,minmax(16rem,1fr))}
.grid-2{grid-template-columns:repeat(auto-fit,minmax(20rem,1fr))}

h1,h2,h3{font-family:var(--display); font-weight:400; line-height:1.15; margin:0}
h1{font-size:clamp(2rem,6vw,3.4rem); letter-spacing:-.01em}
h2{font-size:clamp(1.45rem,3.6vw,2.1rem)}
h3{font-size:1.1rem; font-family:var(--body); font-weight:600}
p{margin:0 0 var(--s4)}
.lede{font-size:clamp(1.02rem,2.2vw,1.2rem); color:var(--ink-muted)}
.k{font-family:var(--mono); font-size:.85em; color:var(--ink-muted)}
.eyebrow{
  font-family:var(--mono); font-size:.75rem; letter-spacing:.14em; text-transform:uppercase;
  color:var(--accent); margin-bottom:var(--s3);
}
.foot{
  border-top:1px solid var(--border); margin-top:var(--s8); padding:var(--s5) 0;
  color:var(--ink-muted); font-size:.87rem;
  display:flex; gap:var(--s4); flex-wrap:wrap; align-items:center;
}

/* ── auth pages ──────────────────────────────────────────────────────────── */
.auth{
  min-height:100dvh; display:grid; place-items:center; padding:var(--s5) var(--s4);
  /* 🔴 Positioned, with a z-index. The arch canvas behind it is position:fixed with z-index:0, and a
     POSITIONED element at z-index 0 paints above ordinary in-flow text however late that text comes
     in the document. Without this the card's words render underneath the background. */
  position:relative; z-index:1;
}
.auth-card{width:min(100%,26rem)}
.auth-card .card{padding:var(--s6)}
.auth-head{text-align:center; margin-bottom:var(--s5)}
.auth-head img{width:64px; height:64px; margin:0 auto var(--s3); border-radius:var(--pill)}
.stack{display:grid; gap:var(--s4)}
.alt{text-align:center; margin-top:var(--s5); color:var(--ink-muted); font-size:.9rem}

@media (max-width:34rem){
  .nav{flex-wrap:wrap; gap:var(--s3)}
  .nav .spacer{display:none}
  .nav-links{width:100%; justify-content:flex-start}
}

/* Anybody who has asked their system not to animate gets no animation. */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.01ms !important; transition-duration:.01ms !important}
}

/* ── the small nav controls ──────────────────────────────────────────────────
 * The theme switch and the GitHub link are two different elements — a <button> the script builds and
 * a plain <a> in the markup — that have to look like ONE control repeated, because they sit side by
 * side in the same glass bar. So the chrome is written once, for both selectors, rather than the
 * anchor getting a copy of these six declarations that drifts the first time either is touched.
 *
 * The theme switch is built by heros-theme.js into whatever carries [data-theme-slot], so its markup
 * exists once instead of once per page. It is a three-state cycle — System, Light, Dark — and the
 * label names the state it is IN, with the accessible name carrying where pressing it goes.
 */
.theme-toggle,.icon-link{
  display:inline-flex; align-items:center; gap:.45rem;
  padding:.4rem .7rem; min-height:36px;
  background:transparent; border:1px solid var(--border); border-radius:var(--pill);
  color:var(--ink-muted); font:inherit; font-size:.8rem; font-weight:500;
  cursor:pointer; transition:color var(--fast), border-color var(--fast), background var(--fast);
  white-space:nowrap;
}
.theme-toggle:hover,.icon-link:hover{color:var(--ink); border-color:var(--border-strong); background:var(--secondary)}
.theme-toggle svg,.icon-link svg{flex:none}
/* Icon-only, so it is square rather than a pill with room for a word beside the glyph. It keeps the
   theme switch's 36px height, which is what makes the two read as a pair. */
.icon-link{justify-content:center; width:36px; padding:.4rem; text-decoration:none}
.icon-link:hover{text-decoration:none}
/* Under about 30rem the two words in the nav are what pushes the sign-in pill onto its own line, and
   the icon alone still says which theme you are in. */
@media (max-width:30rem){ .theme-toggle .theme-name{display:none} .theme-toggle{padding:.4rem .5rem} }
/* 🔴 And under 30rem the bar is FULL: at 375px the wordmark, the switch and the sign-in pill leave
   about 24px of room, so a fourth control — any fourth control — pushes the pill onto a row of its
   own. That is the exact failure the 60rem rule in the page was written to stop, and buying the room
   back by shaving the gap, the pill's padding and the icon at once leaves under 2px of slack, which
   the first font that loads differently would spend. So the icon steps out of the bar on a phone and
   the footer link carries the source instead — it is present on every width. */
@media (max-width:30rem){ .bar .icon-link{display:none} }

/* A dock for pages with no nav to hang it in — the two auth pages. */
.theme-dock{position:fixed; top:var(--s4); right:var(--s4); z-index:40}

/* ── the predictive arch ─────────────────────────────────────────────────────
 * The canvas is DECORATION: aria-hidden in the markup, and it must never take a pointer event or
 * cover content. It is painted by heros-arc.js; this only positions it.
 *
 * 🔴 The mask is what keeps it a background rather than a picture with a hard edge. Without it the
 * lattice stops dead at the bottom of the canvas and the seam is the first thing you see.
 */
.arc-bg{
  position:absolute; inset:0 0 auto 0; height:min(98vh,54rem);
  width:100%; display:block; pointer-events:none; user-select:none; z-index:0;
  /* 🔴 Strength is a VARIABLE, not an opacity typed per rule. Three things want to move it — the
     narrow-viewport rule below, the console's quieter setting, and any future surface — and as plain
     `opacity` declarations they are three rules of identical specificity whose winner is decided by
     source order. That is a coin toss nobody documents: adding the console's rule above the phone's
     silently un-dims the phone. A custom property cascades the same way but composes, so each
     surface states its own value and the narrow-viewport override lands on top of whichever applies. */
  opacity:var(--arc-veil,1);
  -webkit-mask-image:linear-gradient(to bottom,#000 0%,#000 74%,transparent 100%);
  mask-image:linear-gradient(to bottom,#000 0%,#000 74%,transparent 100%);
}
.arc-host{position:relative}
/* Everything the host holds sits above the arch. Stated on the host's children rather than by giving
   each section its own z-index, so a section added later cannot forget and land underneath. */
.arc-host > *:not(.arc-bg){position:relative; z-index:1}

/* The auth pages have no shell to put it inside, so theirs is fixed to the viewport. */
.arc-fixed{position:fixed; inset:0 0 auto 0; height:min(100vh,56rem)}

/* 🔴 Quieter and shorter on a phone, and this is not taste. On a wide window the arch sits ABOVE and
   BESIDE the copy — crown over the headline, feet out past the columns — so it never competes with a
   sentence. Stacked into one narrow column every paragraph lands in the middle of the brightest part
   of it, and the measured result is body text reading through a lattice. The background is decoration;
   the sentence is the product. */
@media (max-width:52rem){
  .arc-bg{--arc-veil:.4; height:min(70vh,32rem)}
}

/* 🔴 Quieter again on a surface people READ rather than look at. The console is a conversation: the
   answer you are waiting for arrives at the top of it, over the brightest part of the arch, and a
   paragraph read through a lattice is a worse product than a plainer background. Half strength keeps
   the page recognisably the same page without competing with a sentence.
   Paired with .arc-fixed on the console it stays under the whole conversation, so this value is not
   a first impression — it is what every reply is read over. It was .5 and damon reported text as
   hard to read against a real transcript; the fix is this AND the text halo in app/index.html, not
   either alone. */
.arc-soft{--arc-veil:.34}
@media (max-width:52rem){
  .arc-soft{--arc-veil:.2}
}
