/* ==========================================================================
   Online Gamers Society
   One stylesheet, no framework, no external fetches. Everything the browser
   needs arrives from this origin, so no third party ever sees a visitor.
   ========================================================================== */

/* --------------------------------------------------------------- tokens -- */
/* Two themes, one shape. Every color below is a role (page bg, raised
   surface, dim label...) rather than a fixed value - :root holds the dark
   palette (the default), [data-theme="light"] restates the same roles with
   light values. JS (theme.js) sets data-theme on <html> before first paint
   from localStorage, defaulting to dark; the toggle at the bottom of the
   Den sidebar flips it. A few tokens (--accent-bright, --on-accent,
   --backdrop, --line-hot, --accent-wash) are deliberately identical in both
   blocks - they're used for glows/fills/borders where a constant vivid
   value reads fine on either background, not for body text. */
:root {
  /* Depth. Was near-black (#05070a) - a full dark-mode UI at that low a
     luminance reads as "too dark" even by dark-theme standards (no OLED
     black-crush concern here, this is a lit LCD/OLED phone or monitor
     either way); lifted the whole ramp several steps toward a normal
     "dark slate" register (closer to GitHub/VS Code dark) while keeping
     the cold cast. */
  --ink-000: #10151c;
  --ink-100: #141a23;
  --ink-200: #181f2a;
  --ink-300: #1e2735;
  --ink-400: #2a3547;

  --line: rgba(150, 190, 215, 0.12);
  --line-mid: rgba(150, 190, 215, 0.20);
  /* Constant across both themes - a low-alpha accent-tinted border/glow
     tint reads fine composited over either a dark or a light surface, and
     it is never used as text, so it doesn't need AA contrast math. */
  --line-hot: rgba(95, 240, 196, 0.38);
  --line-soft: rgba(150, 190, 215, 0.10);
  --line-strong: rgba(150, 190, 215, 0.32);
  --fill-hover: rgba(150, 190, 215, 0.12);

  --fg: #c7d2df;
  --fg-strong: #f3f7fb;
  /* 6.9:1 against the new --ink-000 - real margin above the 4.5:1 AA floor
     this token's actual uses (labels, table headers, nav) require. */
  --fg-dim: #91a0b4;
  /* 4.7:1 - still clears the full text floor, not just the relaxed 3:1
     "large text" one, since a couple of its uses (chart axis labels) run
     small. */
  --fg-faint: #748294;

  /* --accent is the readable-as-text/graphic version (nav links, badges,
     stat values, icons) - identical to --accent-bright in this dark
     theme, but the light theme below deepens it for contrast while
     --accent-bright stays the punchy brand mint for solid fills. */
  --accent: #5ff0c4;
  --accent-bright: #5ff0c4;
  --accent-deep: #23c79b;
  --accent-wash: rgba(95, 240, 196, 0.12);
  /* Text sitting on top of a solid --accent-bright fill (primary button,
     copy-flag tooltip, text selection) - always dark, regardless of
     theme, since the fill itself never darkens. */
  --on-accent: #04140f;
  --ember: #ffab6b;
  --warn: #ff6b7a;

  /* Translucent washes of the page background itself, for surfaces that
     sit over the animated lattice (topbar, inputs, code blocks) and need
     to track whichever --ink-000 is active rather than a fixed color. */
  --wash-1: rgba(16, 21, 28, 0.95);
  --wash-2: rgba(16, 21, 28, 0.92);
  --wash-3: rgba(16, 21, 28, 0.72);
  --wash-4: rgba(16, 21, 28, 0.60);
  /* Modal backdrop dimmer - deliberately constant across both themes (a
     dark "spotlight" scrim behind a dialog is normal even on a light
     page); only the card inside it uses themed tokens. */
  --backdrop: rgba(8, 10, 14, 0.93);

  --panel-glass: linear-gradient(180deg, rgba(30, 39, 53, 0.62), rgba(20, 26, 35, 0.42));
  --panel-solid: linear-gradient(180deg, rgba(30, 39, 53, 0.95), rgba(20, 26, 35, 0.95));
  --surface-soft: rgba(30, 39, 53, 0.42);
  --chart-fill: rgba(95, 240, 196, 0.55);

  --font-display: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Mono", "Segoe UI Mono", Menlo,
    Consolas, "Liberation Mono", monospace;

  --measure: 1180px;
  --gutter: clamp(1.25rem, 4vw, 3.5rem);
  --radius: 3px;

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --dur: 0.55s;

  color-scheme: dark;
}

/* Same roles, light values. A soft off-white "instrument panel" register
   rather than stark white - keeps the shell material feel and avoids
   glare, while every text/surface pairing below is checked to clear
   4.5:1 (or the 3:1 floor for large/decorative text) same as the dark
   palette's own comments document. */
:root[data-theme="light"] {
  --ink-000: #f3f5f7;
  --ink-100: #eaedf1;
  --ink-200: #e2e6eb;
  --ink-300: #d7dde3;
  --ink-400: #c3ccd6;

  --line: rgba(30, 41, 54, 0.12);
  --line-mid: rgba(30, 41, 54, 0.20);
  --line-soft: rgba(30, 41, 54, 0.09);
  --line-strong: rgba(30, 41, 54, 0.32);
  --fill-hover: rgba(30, 41, 54, 0.08);

  --fg: #2b3542;
  --fg-strong: #10151c;
  /* 5.6:1 against the light --ink-000. */
  --fg-dim: #566373;
  /* 3.7:1 - decorative/large-text uses only (same role as dark theme). */
  --fg-faint: #71808f;

  /* Deepened from the brand mint (#5ff0c4, ~1.6:1 on white - unreadable
     as text) to a color that still reads as "the mint accent" but clears
     4.5:1 against both the page and raised card surfaces. */
  --accent: #087a5a;
  --accent-deep: #0c7d5f;
  --ember: #a3500a;
  --warn: #c62839;

  --wash-1: rgba(243, 245, 247, 0.95);
  --wash-2: rgba(243, 245, 247, 0.92);
  --wash-3: rgba(243, 245, 247, 0.72);
  --wash-4: rgba(243, 245, 247, 0.60);

  --panel-glass: linear-gradient(180deg, rgba(215, 221, 227, 0.55), rgba(234, 237, 241, 0.55));
  --panel-solid: linear-gradient(180deg, rgba(215, 221, 227, 0.95), rgba(234, 237, 241, 0.95));
  --surface-soft: rgba(215, 221, 227, 0.5);
  /* A translucent mint bar reads as a near-invisible pastel smear on a
     light panel (checked: ~1.1:1) - solid and deepened here instead of
     just re-tinted, so the "mail received" bars stay legible data, not
     decoration. */
  --chart-fill: #0a6b51;

  color-scheme: light;
}

/* ---------------------------------------------------------------- reset -- */
*,
*::before,
*::after { box-sizing: border-box; }

/* Every element below sets its own margin explicitly wherever it wants
   space (search this file for the class name) - the UA default paragraph
   margin was never one of those choices, it was just never overridden, so
   .field__error, .eyebrow, .lede and a few others were each quietly
   carrying an extra ~1em (top AND bottom) nobody asked for. That is a
   couple of stray lines in a spacious hero; stacked across a dense form or
   dashboard it reads as "everything has too much air around it". Kill the
   default once, here, instead of chasing it class by class. */
p { margin: 0; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

/* The browser's own [hidden] rule lives in the lowest-priority UA
   stylesheet, so ANY author rule setting `display` on the same element -
   .form-grid{display:grid}, .field__row{display:flex}, etc. - silently
   wins and the element stays visible. Toggling `hidden` is used all over
   the den's JS specifically because it is supposed to be unconditional;
   enforce that here once instead of chasing every future collision. */
[hidden] { display: none !important; }

body {
  margin: 0;
  min-height: 100vh;
  background: var(--ink-000);
  color: var(--fg);
  font-family: var(--font-display);
  font-size: clamp(0.95rem, 0.9rem + 0.2vw, 1.05rem);
  line-height: 1.55;
  letter-spacing: -0.006em;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img, svg { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; }

::selection { background: var(--accent-bright); color: var(--on-accent); }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

/* ----------------------------------------------------------- atmosphere -- */
/* Three stacked, pointer-transparent layers: the animated lattice canvas, a
   film grain, and a vignette. Together they stop a huge dark panel from
   reading as a flat void. */
#lattice {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
  opacity: 0.85;
}

.veil {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}

.veil--grain {
  /* Self-contained SVG turbulence: grain without shipping an image file. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
  opacity: 0.035;
  mix-blend-mode: overlay;
}

.veil--vignette {
  background:
    radial-gradient(ellipse 120% 80% at 50% 0%, rgba(95, 240, 196, 0.055), transparent 62%),
    radial-gradient(ellipse 90% 70% at 50% 110%, rgba(60, 120, 220, 0.05), transparent 60%),
    radial-gradient(ellipse 140% 100% at 50% 50%, transparent 45%, rgba(2, 3, 5, 0.75) 100%);
}

/* The dark vignette's own edge-darkening (rgba(2,3,5,0.75) above) reads as
   deliberate atmosphere on a dark page; the same near-black corners over a
   light one just look like a rendering bug. Swap it for a much softer,
   neutral-grey falloff that still gives the page a hint of depth. */
:root[data-theme="light"] .veil--vignette {
  background:
    radial-gradient(ellipse 120% 80% at 50% 0%, rgba(8, 122, 90, 0.05), transparent 62%),
    radial-gradient(ellipse 90% 70% at 50% 110%, rgba(60, 120, 220, 0.04), transparent 60%),
    radial-gradient(ellipse 140% 100% at 50% 50%, transparent 55%, rgba(90, 100, 112, 0.10) 100%);
}

/* The animated point field is tuned to glow against near-black; at the
   same opacity over a light page it reads as visual noise rather than
   atmosphere, so it's dialed back rather than removed - the "cool shell"
   feel stays without competing with foreground text. */
:root[data-theme="light"] #lattice { opacity: 0.4; }

/* ---------------------------------------------------------------- shell -- */
.shell {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.wrap {
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* An ultrawide monitor should get breathing room, not a 3440px-wide line of
   text. The measure above caps it; this keeps the caps honest. */
.wrap--narrow { max-width: 720px; }

/* ------------------------------------------------------------------ nav -- */
.topbar {
  position: sticky;
  top: 0;
  z-index: 40;
  backdrop-filter: blur(14px) saturate(140%);
  background: linear-gradient(to bottom, var(--wash-2), var(--wash-4));
  border-bottom: 1px solid transparent;
  transition: border-color 0.3s var(--ease), background 0.3s var(--ease);
}

.topbar.is-stuck {
  border-bottom-color: var(--line);
  background: var(--wash-1);
}

.topbar__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  height: 62px;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--fg-strong);
}

.brand__mark {
  width: 26px;
  height: 26px;
  flex: none;
  color: var(--accent);
  transition: transform 0.8s var(--ease);
}

.brand:hover .brand__mark { transform: rotate(120deg); }

.brand__text { opacity: 0.92; }
.brand__short { display: none; }

.nav {
  display: flex;
  align-items: center;
  gap: clamp(0.9rem, 2.2vw, 2rem);
  font-family: var(--font-mono);
  font-size: 0.85rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

.nav a { color: var(--fg-dim); transition: color 0.2s; position: relative; }
.nav a:hover, .nav a[aria-current="page"] { color: var(--fg-strong); }

.nav a[aria-current="page"]::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -6px;
  height: 1px;
  background: var(--accent-bright);
}

/* --------------------------------------------------------------- pieces -- */
.eyebrow {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 0.9rem;
}

.eyebrow::before {
  content: "";
  width: 32px;
  height: 1px;
  background: currentColor;
  opacity: 0.6;
  flex: none;
}

.display {
  font-size: clamp(2.5rem, 1.5rem + 4.6vw, 4.9rem);
  line-height: 0.94;
  letter-spacing: -0.045em;
  font-weight: 700;
  color: var(--fg-strong);
  margin: 0;
  text-wrap: balance;
}

.display em {
  font-style: normal;
  color: var(--accent);
}

.lede {
  font-size: clamp(1.02rem, 0.95rem + 0.4vw, 1.2rem);
  color: var(--fg);
  max-width: 54ch;
  text-wrap: pretty;
}


.section {
  padding-block: clamp(1.9rem, 3.2vw, 3rem);
  position: relative;
}

.section + .section { border-top: 1px solid var(--line); }

.section__head {
  display: grid;
  gap: 0.6rem;
  margin-bottom: clamp(1rem, 1.8vw, 1.5rem);
}

.section__title {
  font-size: clamp(1.7rem, 1.2rem + 1.6vw, 2.6rem);
  line-height: 1.1;
  letter-spacing: -0.035em;
  font-weight: 650;
  color: var(--fg-strong);
  margin: 0;
}

/* -------------------------------------------------------------- buttons -- */
.btn {
  --btn-fg: var(--on-accent);
  --btn-bg: var(--accent-bright);
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.9rem 1.6rem;
  border: 1px solid var(--btn-bg);
  border-radius: var(--radius);
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-family: var(--font-mono);
  font-size: 0.85rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
  overflow: hidden;
  transition: transform 0.2s var(--ease), box-shadow 0.3s var(--ease),
    background 0.25s, color 0.25s, border-color 0.25s, opacity 0.2s;
}

.btn:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: 0 12px 34px -14px rgba(95, 240, 196, 0.75);
}

.btn:active:not(:disabled) { transform: translateY(0); }

.btn:disabled { opacity: 0.45; cursor: not-allowed; }

.btn--ghost {
  --btn-bg: transparent;
  --btn-fg: var(--fg-strong);
  border-color: var(--line-mid);
}

.btn--ghost:hover:not(:disabled) {
  border-color: var(--line-hot);
  color: var(--accent);
  box-shadow: none;
}

.btn--block { width: 100%; justify-content: center; }

.btn__arrow { transition: transform 0.3s var(--ease); }
.btn:hover .btn__arrow { transform: translateX(4px); }

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem;
  align-items: center;
}
.actions--detail { margin-top: 1.1rem; }

.btn--push-right { margin-left: auto; }

/* Console readout: honest status, not fabricated stats. */
.readout--lg { font-size: 0.85rem; }
.readout--lg .readout__body { padding: 0.9rem 1.05rem 1rem; }
.readout--lg .readout__line { padding: 0.38rem 0; }

.readout {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-glass);
  font-family: var(--font-mono);
  font-size: 0.8rem;
  overflow: hidden;
}

.readout__bar {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.6rem 0.9rem;
  border-bottom: 1px solid var(--line);
  color: var(--fg-dim);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  font-size: 0.75rem;
}

.readout__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent-bright);
  box-shadow: 0 0 0 0 rgba(95, 240, 196, 0.6);
  animation: pulse 2.8s var(--ease) infinite;
  flex: none;
}

@keyframes pulse {
  0%   { box-shadow: 0 0 0 0 rgba(95, 240, 196, 0.5); }
  70%  { box-shadow: 0 0 0 9px rgba(95, 240, 196, 0); }
  100% { box-shadow: 0 0 0 0 rgba(95, 240, 196, 0); }
}

.readout__clock {
  margin-left: auto;
  color: var(--fg-dim);
  letter-spacing: 0.14em;
  font-variant-numeric: tabular-nums;
}

.readout__body { padding: 0.7rem 0.85rem 0.8rem; }

.readout__line {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.26rem 0;
  color: var(--fg-dim);
  border-bottom: 1px dashed var(--line-soft);
}

.readout__line:last-child { border-bottom: 0; }
.readout__line b { font-weight: 500; color: var(--fg-strong); }
.readout__line b.is-accent { color: var(--accent); }
.readout__line b.is-ember { color: var(--ember); }

.readout__log {
  margin: 0.9rem 0 0;
  padding: 0.75rem 0 0;
  border-top: 1px solid var(--line);
  color: var(--fg-dim);
  font-size: 0.75rem;
  min-height: 3.2em;
  white-space: pre-wrap;
  word-break: break-word;
}

.caret::after {
  content: "\2588";
  color: var(--accent);
  animation: blink 1.1s steps(2, start) infinite;
}

@keyframes blink { 50% { opacity: 0; } }

/* -------------------------------------------------------------- feature -- */
/* One tool per block. Text on the left, a moving thing on the right, so the
   section reads as "something is running here" without asking anyone to read. */
.feature {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
  align-items: center;
  gap: clamp(1.5rem, 4vw, 3.5rem);
  padding: clamp(1.2rem, 2.2vw, 1.75rem);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: linear-gradient(180deg, var(--ink-200), var(--ink-100));
  overflow: hidden;
  position: relative;
  transition: border-color 0.4s var(--ease);
}

.feature:hover { border-color: var(--line-hot); }

.feature::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(
    600px circle at var(--mx, 70%) var(--my, 20%),
    rgba(95, 240, 196, 0.07),
    transparent 45%
  );
  opacity: 0;
  transition: opacity 0.5s var(--ease);
  pointer-events: none;
}

.feature:hover::before { opacity: 1; }

.feature__body {
  position: relative;
  display: grid;
  gap: 0.65rem;
  justify-items: start;
}

.feature__game {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--fg-dim);
  margin: 0;
}

.feature__name {
  margin: 0;
  font-size: clamp(1.6rem, 1.2rem + 1.4vw, 2.3rem);
  font-weight: 650;
  letter-spacing: -0.035em;
  color: var(--fg-strong);
  line-height: 1.05;
}

.feature__text {
  margin: 0;
  color: var(--fg-dim);
  font-size: 0.95rem;
  max-width: 40ch;
}

.feature__host {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--fg-dim);
  margin: 0;
  word-break: break-all;
}

.feature__viz {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 160px;
}

/* ------------------------------------------------------------ mechanism -- */
/* Pure SVG + CSS. Nested rings turning against each other around a live core.
   No JS, no image file, and it stops dead under prefers-reduced-motion. */
.mechanism {
  width: min(100%, 260px);
  height: auto;
  overflow: visible;
  color: var(--accent);
}

.mechanism g,
.mechanism circle,
.mechanism path { transform-box: view-box; transform-origin: 120px 120px; }

.mech__ring { fill: none; stroke: currentColor; }

.mech__ring--outer {
  stroke-opacity: 0.28;
  stroke-width: 1;
  animation: mech-spin 44s linear infinite;
}

.mech__ring--mid {
  stroke-opacity: 0.5;
  stroke-width: 1.1;
  animation: mech-spin 26s linear infinite reverse;
}

.mech__ring--inner {
  stroke-opacity: 0.85;
  stroke-width: 1.2;
  animation: mech-spin 17s linear infinite;
}

.mech__dashes {
  fill: none;
  stroke: currentColor;
  stroke-opacity: 0.35;
  stroke-width: 1;
  stroke-dasharray: 2 10;
  stroke-linecap: round;
  animation: mech-drift 30s linear infinite;
}

.mech__orbit { animation: mech-spin 11s linear infinite; }
.mech__orbit--slow { animation-duration: 19s; animation-direction: reverse; }

.mech__node { fill: currentColor; }

.mech__core {
  fill: currentColor;
  animation: mech-breathe 3.4s var(--ease) infinite;
}

.mech__halo {
  fill: none;
  stroke: currentColor;
  stroke-width: 1;
  animation: mech-ping 3.4s var(--ease) infinite;
}

@keyframes mech-spin {
  to { transform: rotate(360deg); }
}

@keyframes mech-drift {
  to { stroke-dashoffset: -240; }
}

@keyframes mech-breathe {
  0%, 100% { opacity: 0.55; transform: scale(1); }
  50%      { opacity: 1;    transform: scale(1.25); }
}

@keyframes mech-ping {
  0%   { r: 6;  opacity: 0.7; }
  70%  { r: 26; opacity: 0; }
  100% { r: 26; opacity: 0; }
}

.pill {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.32rem 0.65rem;
  border: 1px solid var(--line-mid);
  border-radius: 100px;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--fg-dim);
  white-space: nowrap;
}

.pill--live { color: var(--accent); border-color: var(--line-hot); }
.pill--live::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
}

/* -------------------------------------------------------------- console -- */
/* The landing page is a single screen: instrument on the left, readout on the
   right, footer pinned underneath. .shell is already a flex column at 100vh,
   so flex:1 here makes this fill exactly the gap between bar and footer
   without hard-coding either of their heights. */
.console-screen {
  flex: 1;
  display: flex;
  align-items: center;
  padding-block: clamp(1.75rem, 5vh, 3.5rem);
}

.console {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.92fr);
  align-items: center;
  gap: clamp(2rem, 6vw, 5.5rem);
  width: 100%;
}

.console__viz {
  display: grid;
  justify-items: center;
  gap: 1.6rem;
}

.mechanism--xl { width: min(100%, 360px); }

/* Set as a nameplate rather than a heading: it is the device's name, not a
   sentence, so it cannot read as a slogan. */
.console__mark {
  margin: 0;
  font-family: var(--font-mono);
  font-size: clamp(0.68rem, 0.63rem + 0.2vw, 0.8rem);
  letter-spacing: 0.44em;
  text-transform: uppercase;
  color: var(--fg-dim);
  text-align: center;
  /* letter-spacing adds a trailing gap; nudge back so it optically centres */
  text-indent: 0.44em;
}

.console__panel { display: grid; gap: 1.5rem; }

/* ---------------------------------------------------------------- panel -- */
.panel {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-solid);
  backdrop-filter: blur(14px) saturate(140%);
  padding: clamp(1.35rem, 2.6vw, 1.9rem);
  position: relative;
  overflow: hidden;
}

.panel::after {
  /* hairline of light along the top edge */
  content: "";
  position: absolute;
  top: 0;
  left: 12%;
  right: 12%;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--line-hot), transparent);
}

.panel__head {
  display: grid;
  gap: 0.5rem;
  margin-bottom: 1.35rem;
}

.panel--danger { border-color: rgba(255, 107, 122, 0.3); margin-top: 2.25rem; }
.panel--danger::after { background: linear-gradient(90deg, transparent, var(--warn), transparent); }
.panel--danger .den-subhead { margin-bottom: 0.5rem; }
.panel--danger .field__hint { margin-bottom: 1.1rem; }

.panel__title {
  margin: 0;
  font-size: clamp(1.6rem, 1.2rem + 1.2vw, 2.1rem);
  letter-spacing: -0.035em;
  font-weight: 650;
  color: var(--fg-strong);
}

/* A recurring sub-section heading within a dashboard page ("Members",
   "Invite codes", "New login") - .section__title/.panel__title are sized
   for an actual page heading (up to 2.6rem) and were getting a one-off
   inline font-size override wherever they got reused for something this
   much smaller. One properly-sized class instead of N inline patches. */
.den-subhead {
  margin: 0 0 0.9rem;
  font-size: 1.15rem;
  letter-spacing: -0.02em;
  font-weight: 650;
  color: var(--fg-strong);
}

.panel__sub { margin: 0; color: var(--fg-dim); font-size: 0.93rem; }

.gate-page { display: grid; place-items: center; flex: 1; padding-block: clamp(1.25rem, 3.5vh, 2.25rem); }

/* --------------------------------------------------------------- fields -- */
.form { display: grid; gap: 1rem; }

.field { display: grid; gap: 0.5rem; }

.field__label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--fg-dim);
}

.field__hint { text-transform: none; letter-spacing: 0.02em; color: var(--fg-dim); }

/* Marks a required field on the label itself, the conventional spot -
   .field__hint is for the reverse case (an aside on an optional field, e.g.
   "10 characters minimum"), not a place to also announce "required" on
   every other field once optional ones stop being singled out. */
.field__required { margin-left: 0.25em; color: var(--warn); }

.input {
  width: 100%;
  padding: 0.85rem 1rem;
  border: 1px solid var(--line-mid);
  border-radius: var(--radius);
  background: var(--wash-3);
  color: var(--fg-strong);
  font-family: var(--font-mono);
  font-size: 0.92rem;
  letter-spacing: 0.02em;
  transition: border-color 0.25s, box-shadow 0.25s, background 0.25s;
}

.input::placeholder { color: var(--fg-faint); letter-spacing: 0.08em; }

.input:hover { border-color: var(--line-strong); }

.input:focus {
  outline: none;
  border-color: var(--accent-deep);
  background: var(--wash-1);
  box-shadow: 0 0 0 3px var(--accent-wash);
}

.input[aria-invalid="true"] {
  border-color: var(--warn);
  box-shadow: 0 0 0 3px rgba(255, 107, 122, 0.12);
}

.input--code {
  letter-spacing: 0.3em;
  text-transform: uppercase;
  font-size: 1rem;
}

/* Honeypot. Present in the DOM, invisible and unreachable for humans;
   scripted submitters fill it in and get quietly discarded. */
.trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.field__error {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  color: var(--warn);
  min-height: 1em;
}

/* Passphrase strength */
.strength { display: grid; gap: 0.4rem; }

.strength__track {
  display: flex;
  gap: 4px;
  height: 3px;
}

.strength__seg {
  flex: 1;
  background: var(--ink-400);
  border-radius: 2px;
  transition: background 0.35s var(--ease);
}

.strength__seg.is-on { background: var(--accent-bright); }
.strength__seg.is-weak { background: var(--warn); }
.strength__seg.is-fair { background: var(--ember); }

.strength__label {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg-dim);
}

/* --------------------------------------------------------------- notice -- */
.notice {
  display: none;
  gap: 0.75rem;
  align-items: flex-start;
  padding: 0.95rem 1.1rem;
  border: 1px solid var(--line-mid);
  border-left-width: 2px;
  border-radius: var(--radius);
  font-size: 0.9rem;
  background: var(--wash-4);
}

.notice.is-shown { display: flex; }
.notice--ok { border-left-color: var(--accent); color: var(--fg-strong); }
.notice--bad { border-left-color: var(--warn); color: var(--warn); }
.notice--wait { border-left-color: var(--ember); color: var(--ember); }
.notice__icon { flex: none; margin-top: 0.15rem; }

.form__foot {
  display: grid;
  gap: 0.9rem;
  padding-top: 0.4rem;
  border-top: 1px solid var(--line);
}

.fineprint {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  line-height: 1.7;
  color: var(--fg-dim);
  margin: 0;
}

/* The handful of one-off spacing/color tweaks below used to be inline
   style="" attributes. The site's own CSP is style-src 'self' with no
   unsafe-inline and no per-page nonce/hash, so every one of them was
   silently no-op'd by the browser - visually invisible (nothing crashes,
   the margin is just quietly absent), so it went unnoticed. Real classes
   instead, so they actually take effect. */
.fineprint--warn { color: var(--ember); }

.fineprint a { color: var(--fg-dim); border-bottom: 1px solid var(--line-mid); }
.fineprint a:hover { color: var(--accent); border-color: var(--line-hot); }

/* ------------------------------------------------------------ cta strip -- */
.cta {
  display: grid;
  gap: 0.85rem;
  justify-items: center;
  text-align: center;
  padding: clamp(1.6rem, 3vw, 2.4rem) clamp(1.25rem, 3vw, 2rem);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background:
    radial-gradient(ellipse 70% 120% at 50% 0%, rgba(95, 240, 196, 0.07), transparent 70%),
    linear-gradient(180deg, var(--ink-200), var(--ink-000));
}

.cta .lede { text-align: center; }

/* --------------------------------------------------------------- footer -- */
.footer {
  margin-top: auto;
  border-top: 1px solid var(--line);
  padding-block: 1.1rem 1.35rem;
}

.footer__inner {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem;
  align-items: center;
  justify-content: space-between;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  color: var(--fg-dim);
}

.footer__links { display: flex; flex-wrap: wrap; gap: 1.4rem; }
.footer__links a { transition: color 0.2s; }
.footer__links a:hover { color: var(--accent); }

.cipher {
  font-family: var(--font-mono);
  letter-spacing: 0.24em;
  color: var(--fg-faint);
  cursor: default;
  user-select: none;
}

/* --------------------------------------------------------------- 404 --- */
.void {
  flex: 1;
  display: grid;
  place-items: center;
  text-align: center;
  padding-block: clamp(2.5rem, 8vh, 5rem);
}

.void__code {
  font-family: var(--font-mono);
  font-size: clamp(4rem, 14vw, 9rem);
  line-height: 1;
  letter-spacing: 0.06em;
  color: var(--fg-strong);
  margin: 0 0 1rem;
}

/* ------------------------------------------------------------- easter --- */
.sealed {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: none;
  place-items: center;
  padding: var(--gutter);
  background: var(--backdrop);
  backdrop-filter: blur(8px);
}

.sealed.is-open { display: grid; }

.sealed__card {
  max-width: 540px;
  border: 1px solid var(--line-hot);
  border-radius: var(--radius);
  background: var(--ink-100);
  padding: clamp(1.75rem, 4vw, 2.75rem);
  font-family: var(--font-mono);
  font-size: 0.82rem;
  line-height: 1.9;
  color: var(--fg);
  box-shadow: 0 40px 120px -40px rgba(95, 240, 196, 0.4);
}

.sealed__card h2 {
  margin: 0 0 1rem;
  font-size: 0.78rem;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--accent);
}

.sealed__card p { margin: 0 0 1rem; }
.sealed__card p:last-of-type { margin-bottom: 0; }

.sealed__close {
  margin-top: 1.5rem;
  background: none;
  border: 1px solid var(--line-mid);
  border-radius: var(--radius);
  padding: 0.5rem 1rem;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fg-dim);
  cursor: pointer;
}

.sealed__close:hover { color: var(--accent); border-color: var(--line-hot); }

/* ------------------------------------------------------ confirm modal --- */
/* Same overlay convention as .sealed above, but for a real decision (grant
   root) rather than an easter egg - warn-toned border, an explicit
   cancel/confirm pair instead of one close button. */
.confirm-modal {
  position: fixed;
  inset: 0;
  z-index: 92;
  display: none;
  place-items: center;
  padding: var(--gutter);
  background: var(--backdrop);
  backdrop-filter: blur(8px);
}

.confirm-modal.is-open { display: grid; }

.confirm-modal__card {
  max-width: 480px;
  border: 1px solid rgba(255, 107, 122, 0.4);
  border-radius: var(--radius);
  background: var(--ink-100);
  padding: clamp(1.75rem, 4vw, 2.25rem);
  font-family: var(--font-mono);
  font-size: 0.82rem;
  line-height: 1.9;
  color: var(--fg);
  box-shadow: 0 40px 120px -40px rgba(255, 107, 122, 0.35);
}

.confirm-modal__card h2 {
  margin: 0 0 1rem;
  font-size: 0.78rem;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--warn);
}

.confirm-modal__card p { margin: 0 0 1rem; }

.confirm-modal__actions { display: flex; justify-content: flex-end; gap: 0.75rem; margin-top: 1.5rem; }

/* --------------------------------------------------------- mail modal --- */
/* Same overlay convention as .sealed above (fixed, centred, backdrop
   blur, .is-open toggles display:grid) - a message's iframe is height-
   capped in the inbox list (see .msgframe) so a long one needs its own
   scrollbar there; this is the "give me the whole thing" escape hatch,
   one shared modal reused for whichever message was last expanded rather
   than building one per message up front. */
.mail-modal {
  position: fixed;
  inset: 0;
  z-index: 95;
  display: none;
  place-items: center;
  padding: var(--gutter);
  background: var(--backdrop);
  backdrop-filter: blur(8px);
}

.mail-modal.is-open { display: grid; }

.mail-modal__card {
  width: min(100%, 920px);
  height: min(100%, 88vh);
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line-hot);
  border-radius: var(--radius);
  background: var(--ink-100);
  box-shadow: 0 40px 120px -40px rgba(95, 240, 196, 0.4);
  overflow: hidden;
}

.mail-modal__head {
  flex: none;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem 1.1rem;
  border-bottom: 1px solid var(--line);
}

.mail-modal__heading { min-width: 0; }

.mail-modal__subject {
  margin: 0;
  font-size: 1rem;
  font-weight: 650;
  color: var(--fg-strong);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.mail-modal__meta {
  margin: 0.2rem 0 0;
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: var(--fg-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.mail-modal__head-actions { flex: none; display: flex; align-items: center; gap: 0.6rem; }

.mail-modal__frame { flex: 1; width: 100%; border: 0; background: var(--ink-100); }

@media (max-width: 640px) {
  .mail-modal { padding: 0.75rem; }
}

/* -------------------------------------------------------------- reveal --- */
[data-reveal] {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 0.75s var(--ease), transform 0.75s var(--ease);
}

[data-reveal].is-in { opacity: 1; transform: none; }

/* --------------------------------------------------------- small screens - */
@media (max-width: 900px) {
  .console {
    grid-template-columns: minmax(0, 1fr);
    gap: 2.25rem;
  }
  .mechanism--xl { width: min(62%, 240px); }
  .console-screen { padding-block: clamp(1.5rem, 4vh, 2.5rem); }
  .feature {
    grid-template-columns: minmax(0, 1fr);
    gap: 1.75rem;
  }
  /* The mechanism keeps its presence but stops dominating a narrow column. */
  .feature__viz { min-height: 150px; order: -1; }
  .mechanism { width: min(60%, 190px); }
}

@media (max-width: 560px) {
  .brand__text { display: none; }
  .brand__short { display: inline; }
  .topbar__inner { height: 60px; }
  .nav { gap: 0.9rem; font-size: 0.78rem; }
  .footer__inner { justify-content: flex-start; }
}

/* ------------------------------------------------------- reduced motion -- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  [data-reveal] { opacity: 1; transform: none; }
  #lattice { display: none; }
}

/* Centre the flex-based bits inside the centred 404 column. */
.void .eyebrow,
.void .actions { justify-content: center; }
.void .lede { margin-inline: auto; }
.void .section__title { margin-block: 0 1rem; }

/* ============================================================================
   The Den - authenticated area
   Same instrument-panel language as the public site, now driven by real data
   fetched from /api/. No template engine here either: these pages are static
   shells: JS renders content with textContent/createElement, never innerHTML,
   for anything that came from a user (see den*.js). The one exception is
   rendered email, which never touches this DOM at all - it lives in its own
   sandboxed iframe (see main.py's /render route).
   ========================================================================== */
/* Unlike .topbar__inner (the public nav), this bar has no fixed height, so
   with no padding of its own it was exactly as tall as its tallest child -
   the Log out button - which then touched the top and bottom edge with
   nothing around it. The mobile breakpoint below already knew to add
   padding-block here; the desktop rule just never got it. */
.denbar__inner { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding-block: 1rem; }

.dennav {
  display: flex;
  align-items: center;
  gap: clamp(0.9rem, 2.2vw, 1.75rem);
  font-family: var(--font-mono);
  font-size: 0.9rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.dennav a { color: var(--fg-dim); transition: color 0.2s; }
.dennav a:hover, .dennav a[aria-current="page"] { color: var(--fg-strong); }

/* Two copies of the same nav exist in every den*.html - .dennav--topbar
   (inside the header, today's layout) and .dennav--sidebar (in the new
   .sidebar below). Only one is ever visible at a time; see the breakpoint
   at the bottom of this section. A sandboxed iframe or postMessage-style
   "move this element" trick would be more clever, but a duplicated nav
   toggled by one media query is simpler to read and cannot get its own
   layout logic wrong across two very different flex directions. */
.sidebar .dennav {
  flex-direction: column;
  align-items: stretch;
  gap: 0.2rem;
}
.sidebar .dennav a {
  display: block;
  padding: 0.55rem 0.7rem;
  border-radius: var(--radius);
  transition: background 0.2s, color 0.2s;
}
.sidebar .dennav a:hover { background: var(--line); }
.sidebar .dennav a[aria-current="page"] { background: var(--accent-wash); color: var(--accent); }

/* ------------------------------------------------------------ theme toggle */
/* Sits below the sidebar nav (margin-top:auto pushes it to the bottom of
   the flex column - see .sidebar) rather than in the topbar, matching
   where the user asked for it: a quiet utility, not something competing
   with the primary nav for attention. theme.js sets data-theme on <html>
   before first paint; this button just flips it and persists the choice.
   Icon/label swap is pure CSS (:root[data-theme] visibility), so no JS is
   needed beyond the click handler itself - see initThemeToggle in
   site.js. */
.theme-toggle {
  margin-top: auto;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--line-mid);
  border-radius: var(--radius);
  background: transparent;
  color: var(--fg-dim);
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  transition: border-color 0.2s, color 0.2s, background 0.2s;
}

.theme-toggle:hover { border-color: var(--line-hot); color: var(--fg-strong); background: var(--line); }

.theme-toggle__icon { width: 15px; height: 15px; flex: none; }

/* Both icons/labels exist in the DOM at all times; which pair shows is
   decided purely by which data-theme is active on <html> - toggling the
   button's own visual state needs no JS beyond flipping that attribute.
   The button names the theme a click will switch TO, not the one that's
   already active - showing "Light mode" while already in light mode read
   as a status readout, not a control, so the pair shown is the opposite
   of the current theme. */
:root[data-theme="light"] .theme-toggle__icon--light,
:root[data-theme="light"] .theme-toggle__text--light { display: none; }
:root:not([data-theme="light"]) .theme-toggle__icon--dark,
:root:not([data-theme="light"]) .theme-toggle__text--dark { display: none; }

.userbar { display: flex; align-items: center; gap: 0.9rem; }

.userchip {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  font-family: var(--font-mono);
  font-size: 0.875rem;
  color: var(--fg-dim);
}

.userchip__name { color: var(--fg-strong); }

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.24rem 0.6rem;
  border: 1px solid var(--line-mid);
  border-radius: 100px;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
}

.badge::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }

.badge--rookie { color: var(--fg-dim); border-color: var(--line-mid); }
.badge--operative { color: var(--accent); border-color: var(--line-hot); }
.badge--veteran { color: var(--accent-deep); border-color: rgba(35, 199, 155, 0.4); }
.badge--root { color: var(--ember); border-color: rgba(255, 171, 107, 0.4); }

.btn--sm { padding: 0.55rem 0.95rem; font-size: 0.8125rem; }
.btn--danger { --btn-bg: transparent; --btn-fg: var(--warn); border-color: rgba(255, 107, 122, 0.4); }
.btn--danger:hover:not(:disabled) { border-color: var(--warn); box-shadow: none; }

/* ------------------------------------------------------------------ layout */
/* .den-body is the flex:1 child of .shell now (in place of .den-main
   directly) - a horizontal row holding the sidebar and the actual page
   content side by side. .shell keeps its base min-height:100vh (see
   .shell above) so on a short page (a fresh dashboard, an empty vault)
   the leftover room collects below the content instead of vanishing -
   .den-body's own flex:1 still pushes the footer down to the bottom of
   the viewport rather than letting it ride up right under the content. */
.den-body { display: flex; flex: 1; gap: clamp(1.5rem, 3vw, 2.5rem); }

/* A flex column (not just a block) so .theme-toggle, added as the nav's
   sibling, can pin itself to the bottom via margin-top:auto - .sidebar
   itself is already stretched to the full row height by .den-body's
   default align-items:stretch, so there is real room below a short nav
   list for the toggle to actually sit at the bottom rather than right
   under the last link. */
.sidebar { flex: none; width: 190px; padding-block: clamp(1.75rem, 4vh, 3rem); display: flex; flex-direction: column; }

.den-main { flex: 1; min-width: 0; padding-block: clamp(1.75rem, 4vh, 3rem); }

.den-content { min-width: 0; }

/* Below 900px there is not enough room for a 190px rail plus a usable
   content column, so the sidebar copy of the nav hides and the topbar
   copy (already responsive down through the 720px breakpoint below)
   takes over instead - exactly today's mobile behaviour, unchanged. */
@media (min-width: 900px) { .dennav--topbar { display: none; } }
@media (max-width: 899px) { .sidebar { display: none; } }

/* ----------------------------------------------------- den density scale --
   .btn/.input/.panel's base sizing was tuned for the public marketing
   pages: a handful of dramatic elements, lots of surrounding space, small
   quiet mono-caps labels as accents. A dashboard is the opposite job -
   scan a list, read a value, fill a form, repeatedly - and inherited that
   scale wholesale, which is why button/nav/value text kept reading as
   small relative to how much padding surrounded it. Scoped to
   .shell:has(.den-main) rather than changed on the shared classes
   directly, so /enter, /join and /invite keep their original, deliberately
   spacious look. */
.shell:has(.den-main) {
  /* Den content is tables and forms, not marketing prose - a narrower
     measure just forces the roster/invite tables to wrap their Code
     column and everything else to sit in a tighter well than the
     viewport actually has room for. Wider than the public pages'
     default, still centered with real margin either side above ~1500px. */
  --measure: 1360px;
}

.shell:has(.den-main) .btn {
  padding: 0.6rem 1.15rem;
  font-size: 0.85rem;
  gap: 0.5rem;
}
.shell:has(.den-main) .btn--sm { padding: 0.45rem 0.85rem; font-size: 0.8125rem; }

.shell:has(.den-main) .input { padding: 0.55rem 0.75rem; font-size: 0.95rem; }

.shell:has(.den-main) .panel { padding: 1.1rem 1.3rem; }

/* .panel__title is also the "Join the Society." / "Enter" heading on the
   public gate pages, deliberately large there. A single vault entry's
   "Total Battle" heading doesn't carry the same weight, so it gets its own
   smaller size here instead of changing the shared rule. */
.shell:has(.den-main) .panel__title { font-size: clamp(1.35rem, 1.1rem + 0.7vw, 1.75rem); }

.shell:has(.den-main) .stat { padding: 0.85rem 1rem; }

.den-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: clamp(1.5rem, 3vw, 2.25rem);
}
.den-head--spaced { margin-top: 2.25rem; }

.den-head__title {
  margin: 0;
  font-size: clamp(1.35rem, 1.1rem + 0.7vw, 1.75rem);
  letter-spacing: -0.03em;
  font-weight: 650;
  color: var(--fg-strong);
}

.den-head__sub { margin: 0.3rem 0 0; color: var(--fg-dim); font-size: 0.9rem; }

/* A single login's detail page used to spend an entire .panel's top
   padding plus its own eyebrow+title block (each with their own margin)
   just to say "Login" and repeat the game name - all before a single
   real field was visible. Folded into the same title-row-plus-actions
   shape every other Den page's header already uses (.den-head), with the
   back link demoted to a small icon rather than its own full line above
   the panel. */
.login-detail-title { display: flex; align-items: center; gap: 0.6rem; min-width: 0; }
.login-detail-title .den-head__title { min-width: 0; overflow-wrap: break-word; }

/* -------------------------------------------------------------- stat tiles */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 1rem;
  margin-bottom: clamp(1.5rem, 3vw, 2.25rem);
}

.stat {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-glass);
  padding: 1.1rem 1.2rem;
}

.stat__label {
  margin: 0 0 0.4rem;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--fg-dim);
}

.stat__value { margin: 0; font-size: 1.6rem; font-weight: 650; color: var(--fg-strong); letter-spacing: -0.02em; }
.stat__value.is-accent { color: var(--accent); }
.stat__value.is-ember { color: var(--ember); }

/* -------------------------------------------------------------- mini chart */
.chart-bar { fill: var(--chart-fill); transition: fill 0.15s; }
.chart-bar:hover { fill: var(--accent); }
.chart-bar--empty { fill: var(--ink-400); }
.chart-daylabel {
  fill: var(--fg-faint);
  font-family: var(--font-mono);
  font-size: 3.2px;
}

.chart-panel, .chart-empty { margin-top: 2.25rem; }

/* ---------------------------------------------------------------- toolbar */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.9rem;
  margin-bottom: 1.1rem;
}

.toolbar__hint { margin: 0; font-family: var(--font-mono); font-size: 0.875rem; color: var(--fg-dim); }

/* ------------------------------------------------------------------ empty */
.empty {
  border: 1px dashed var(--line-mid);
  border-radius: var(--radius);
  padding: clamp(1.75rem, 4vw, 2.5rem);
  text-align: center;
  color: var(--fg-dim);
  font-size: 0.9rem;
}

/* -------------------------------------------------------------- the vault */
/* Column headings above the vault list - same grid as .vault__row so the
   labels line up with the values underneath. Mobile stacks each row into
   its own card with an inline label per value (.vault__label) instead, so
   this header would just be a redundant, badly-squeezed row there. */
.vault__head {
  display: grid;
  grid-template-columns:
    minmax(110px, 200px) minmax(180px, 230px) minmax(110px, 150px)
    minmax(0, 1fr) auto;
  gap: 1rem;
  padding: 0 1.1rem 0.5rem;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg-dim);
}

.vault { display: grid; gap: 0.7rem; }

.vault__row {
  display: grid;
  position: relative;
  /* Game/email/password are given a *range*, not just a floor - 1fr-style
     tracks stretch to fill whatever room the row has regardless of how
     short the actual value is, which is exactly what made a row holding
     "test" / "test@mailinpot.com" / dots read as sparse and gap-toothed
     rather than compact. Notes is the one field genuinely meant to be
     free-text and variable-length, so it is the one that absorbs whatever
     space the others don't need - but Email/Password's own *max* used to
     be generous enough (280px/180px) that they'd claim most of the row
     even holding a short value, leaving Notes a sliver. Tightened so a
     short email/password stops eating room Notes actually needs. */
  grid-template-columns:
    minmax(110px, 200px) minmax(180px, 230px) minmax(110px, 150px)
    minmax(0, 1fr) auto;
  align-items: center;
  gap: 1rem;
  padding: 0.9rem 1.1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-glass);
  transition: border-color 0.25s;
}

.vault__row:hover, .vault__row:focus-within { border-color: var(--line-hot); }

/* The whole row opens the login, not just the small chevron at the end -
   a real, empty <a> stretched over the row (first in DOM order) rather
   than a click handler on the row itself, so it's a genuine link: tab
   stop, "open in new tab", status-bar URL preview, all free. Every
   element meant to stay independently clickable (the copy buttons, the
   external game link when the game name looks like a URL) has its own
   position:relative and comes later in DOM order, which is enough for it
   to paint - and receive clicks - above this one; nothing else needs a
   z-index arms race. */
.vault__row-link { position: absolute; inset: 0; border-radius: inherit; }

.vault__cell { min-width: 0; display: grid; gap: 0.2rem; }

/* Holds a copyable value next to its own copy button, so the button reads
   as "copy this specific thing beside me" rather than needing to be found
   in a separate cluster of four look-alike buttons at the far end of the
   row - see .vault__actions below, which now only holds row-level actions
   (open, delete), not per-field ones. */
.vault__cell-row { display: flex; align-items: center; gap: 0.4rem; min-width: 0; }
.vault__cell-row .vault__mono { flex: 1; min-width: 0; }

.vault__label {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fg-dim);
  display: none; /* shown only in the stacked (mobile) layout */
}

.vault__game {
  color: var(--fg-strong);
  font-weight: 550;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* position:relative promotes this above .vault__row-link (see there) so
   it opens the game's own site rather than this row's detail page. */
.vault__game a { position: relative; border-bottom: 1px solid var(--line-mid); }
.vault__game a:hover { color: var(--accent); border-color: var(--line-hot); }

/* Quiet on purpose - a scan-past detail under the game name, not something
   competing with it. The list is already sorted by this (see
   db._list_aliases_for_user), so it mostly confirms what the ordering
   already implies rather than being the first thing read. */
.vault__lastmail {
  display: block;
  margin-top: 0.15rem;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--fg-dim);
}

.vault__mono {
  font-family: var(--font-mono);
  font-size: 0.92rem;
  color: var(--fg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.vault__notes {
  color: var(--fg-dim);
  font-size: 0.88rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The single-entry detail page has room to actually show a multi-line
   note instead of eliding it the way the compact list row has to. */
.vault__notes--wrap { overflow: visible; text-overflow: clip; white-space: pre-wrap; }

.vault__actions { display: flex; align-items: center; gap: 0.4rem; flex: none; }

/* Rotates the chevron open when its preview is showing - same convention
   as .roster-expand/.roster-expand__chev (den-roster.js), kept as its own
   copy rather than sharing that name across two unrelated files. */
.vault__expand-chev { color: var(--fg-faint); transition: transform 0.25s var(--ease); }
.vault__expand.is-open .vault__expand-chev { transform: rotate(90deg); color: var(--accent); }

/* The chevron's own preview drawer - a peek at the newest mail without
   leaving the list, not a second inbox reader. Sits as .vault__row's own
   sibling inside .vault__group (see den-logins.js) rather than as a grid
   item of .vault directly, so it can sit flush under its own row while
   .vault's own gap still separates one login's [row + preview] unit from
   the next login's.

   Deliberately drawn as *one continuous shape with the row above it*,
   not a second separate box: zero margin and no top border/radius so the
   row's own bottom edge is the only seam between them, and both switch to
   the same --line-hot border the instant the drawer opens (see
   .vault__group.is-expanded below) - a shared accent-coloured outline
   reads as "this came from that row" far more clearly than a gap and a
   plain gray box ever would. */
.vault-preview {
  margin-top: 0;
  border: 1px solid var(--line-hot);
  border-top: 0;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
  border-bottom-left-radius: var(--radius);
  border-bottom-right-radius: var(--radius);
  background: var(--surface-soft);
  padding: 0 0.9rem;
}

/* The row's own bottom corners square off to meet the drawer flush, and
   its border picks up the same accent tint the drawer already uses - see
   the comment on .vault-preview above. */
.vault__group.is-expanded .vault__row {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  border-bottom-color: var(--line-hot);
}

.vault-preview__status { margin: 0; padding: 0.9rem 0; color: var(--fg-dim); font-size: 0.85rem; text-align: center; }

.vault-preview__item { padding: 0.65rem 0; border-bottom: 1px dashed var(--line-soft); }
.vault-preview__item:last-of-type { border-bottom: 0; }

.vault-preview__meta { margin: 0; display: flex; align-items: center; gap: 0.5rem; font-family: var(--font-mono); font-size: 0.8rem; }
.vault-preview__from { flex: 1; color: var(--fg-strong); font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vault-preview__date { flex: none; color: var(--fg-dim); }
.vault-preview__expand { flex: none; }
.vault-preview__subject { margin: 0.15rem 0 0; color: var(--fg-dim); font-size: 0.85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.vault-preview__link {
  display: block;
  padding: 0.7rem 0;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent);
  text-align: center;
}
.vault-preview__link:hover { text-decoration: underline; }

.iconbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border: 1px solid var(--line-mid);
  border-radius: var(--radius);
  background: transparent;
  color: var(--fg-dim);
  cursor: pointer;
  transition: border-color 0.2s, color 0.2s;
  position: relative;
  flex: none;
}

.iconbtn:hover { color: var(--accent); border-color: var(--line-hot); }
.iconbtn svg { width: 14px; height: 14px; }

.iconbtn.is-copied { color: var(--accent); border-color: var(--line-hot); }

.iconbtn__flag {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  padding: 0.25rem 0.5rem;
  border-radius: 3px;
  background: var(--accent-bright);
  color: var(--on-accent);
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s, transform 0.15s;
}

.iconbtn.is-copied .iconbtn__flag { opacity: 1; transform: translateX(-50%) translateY(-3px); }

/* A quieter, smaller .iconbtn for a context where the standard 30px
   square would dominate a compact line of text - currently just the
   per-message expand button inside .vault-preview__item. */
.iconbtn--sm { width: 22px; height: 22px; }
.iconbtn--sm svg { width: 12px; height: 12px; }

/* ------------------------------------------------------------ password -- */
/* The reveal toggle sits *inside* the field's own box (the classic
   password-manager eye icon at the right edge), not as a separate button
   beside it - wraps either a real <input> (New Login / Edit forms) or a
   read-only value display (the vault list and the detail page both show
   a password as a styled <p>, not an input) so the exact same look and
   positioning works for both. See D.wirePasswordToggle/wireValueReveal
   in den.js for the click behaviour. */
.password-field { position: relative; flex: 1; min-width: 0; }
.password-field .input { padding-right: 2.5rem; }

.password-field__toggle {
  position: absolute;
  top: 50%;
  right: 0.3rem;
  transform: translateY(-50%);
  width: 26px;
  height: 26px;
  border-color: transparent;
  background: transparent;
}
.password-field__toggle:hover { border-color: var(--line-hot); background: var(--line); }

/* ------------------------------------------------------------ create panel */
.create-panel { margin-bottom: 1.25rem; }
.token-create-panel { margin-bottom: 1.5rem; }
.token-reveal { display: grid; gap: 0.6rem; width: 100%; }
.token-value { display: block; overflow-x: auto; white-space: nowrap; }

/* ---------------------------------------------------------------- code -- */
.codeblock-wrap { position: relative; }

.codeblock {
  margin: 0 0 1.25rem;
  padding: 1.1rem 1.3rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--wash-4);
  font-family: var(--font-mono);
  font-size: 0.85rem;
  line-height: 1.65;
  color: var(--fg);
  overflow-x: auto;
  white-space: pre;
}

/* Only reserve room for the copy button on the one block that actually
   has one, instead of every code sample carrying dead space on its right
   whether or not there's a button sitting over it. */
.codeblock-wrap:has(.codeblock-copy) .codeblock { padding-right: 3rem; }
.codeblock-copy { position: absolute; top: 0.7rem; right: 0.7rem; }

/* An inline code reference sitting in ordinary prose or a table cell - not
   .vault__mono, whose white-space:nowrap and ellipsis are tuned for a
   fixed-width row cell. Some of these strings (a JSON shape, an error
   example) are long enough that they need to wrap like the rest of the
   sentence around them, not overflow it. */
.inline-code {
  font-family: var(--font-mono);
  font-size: 0.85em;
  padding: 0.1em 0.35em;
  border-radius: 3px;
  background: var(--fill-hover);
  color: var(--fg-strong);
}

/* ------------------------------------------------------- api reference -- */
.endpoint-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin-top: 2.25rem;
}
.endpoint-toolbar .den-subhead { margin: 0; }

/* One heading tier below .den-subhead ("API reference" itself) - groups
   endpoints by resource the way most hand-written HTTP API docs do
   (Stripe, GitHub, Twilio: a left-aligned reference, not a table). */
.endpoint-group__title {
  margin: 2.25rem 0 1rem;
  padding-top: 1.75rem;
  border-top: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
}
.endpoint-group__title:first-of-type { margin-top: 1.5rem; padding-top: 0; border-top: 0; }

/* Adjacent-sibling border: only an endpoint that FOLLOWS another one
   inside the same group gets a divider - the first in each group already
   sits right under that group's own title/border, a second one there
   would just double up. */
.endpoint + .endpoint { margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px dashed var(--line-soft); }

.endpoint__head { display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 0.5rem; }

.endpoint__method {
  flex: none;
  display: inline-block;
  min-width: 3.4em;
  text-align: center;
  padding: 0.15rem 0.5rem;
  border-radius: 3px;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 650;
  letter-spacing: 0.04em;
}
.endpoint__method--get { background: var(--accent-wash); color: var(--accent); }
.endpoint__method--post { background: rgba(255, 171, 107, 0.14); color: var(--ember); }

.endpoint__path { font-family: var(--font-mono); font-size: 1rem; font-weight: 600; color: var(--fg-strong); }

.endpoint__desc { margin: 0 0 0.9rem; color: var(--fg-dim); font-size: 0.9rem; max-width: 70ch; }

.endpoint__label {
  margin: 0.9rem 0 0.5rem;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg-dim);
}
.endpoint__label:first-of-type { margin-top: 0; }

.endpoint__status { color: var(--accent); }

/* A standard "parameters" list under a request body example - field name,
   then its rules, the same shape every REST API reference uses this for
   instead of comments jammed inside the JSON itself (which isn't valid
   JSON, and reads as if it might be copy-pasteable when it is not). */
.params { margin: 0 0 0.9rem; display: grid; gap: 0.4rem; }
.params__row { display: grid; grid-template-columns: minmax(100px, max-content) 1fr; gap: 0.5rem 1rem; }
.params__row dt { margin: 0; font-family: var(--font-mono); font-size: 0.85rem; font-weight: 650; color: var(--fg-strong); }
.params__row dd { margin: 0; font-size: 0.85rem; color: var(--fg-dim); }

@media (max-width: 560px) {
  .params__row { grid-template-columns: 1fr; gap: 0.15rem; }
}

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

/* auto-fit only collapses a repeated track when NO item lands in it. A row
   of exactly two short fields (Email, Password) next to a .field--wide
   item that spans grid-column:1/-1 counts as "occupying" every track that
   spanning item passes through - so on a wide panel the two short fields
   still get squeezed into two ~190px tracks while 2-3 more auto-generated
   tracks sit empty beside them (the spanning item is happy to stretch, it
   never signals that those tracks were unwanted). The visible result was
   an email address elided to "rash-engine@..." next to several hundred
   idle pixels. A view that is always exactly "two short fields, then one
   wide one" doesn't need the auto-fit guess at all - just say two columns
   and let them use the room. */
.form-grid--pair { grid-template-columns: repeat(2, minmax(0, 1fr)); }

@media (max-width: 560px) {
  .form-grid--pair { grid-template-columns: 1fr; }
}

/* Game / Address+Domain combo / Password on one row, Notes given its own
   full row below (via .form-grid .field--wide, already used for exactly
   this elsewhere). Address and Domain used to be two separate columns
   here; they're now one merged .addr-combo control (see below), so this
   is three tracks, not four - the combo's own middle column gets the
   most room since it's doing the work of two fields at once. */
.form-grid--create {
  grid-template-columns:
    minmax(150px, 1fr) minmax(240px, 1.3fr) minmax(140px, 0.9fr);
}

/* This form lives inside .den-main, which shares the viewport with a
   190px .sidebar from 900px up (see .den-body) - the three columns above
   need a viewport closer to ~1100px to actually have that much room once
   the sidebar, page gutters and panel padding are all accounted for.
   Two columns (2x2, Notes still spanning the full row below) comfortably
   fits the gap between "narrow enough the sidebar has already hidden
   itself" and "wide enough for three". */
@media (max-width: 1100px) {
  .form-grid--create { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
  .form-grid--create { grid-template-columns: 1fr; }
}

/* Grid items default to min-width:auto, same trap flex items have (see
   .field__row .input above) - minmax(190px, 1fr) bounds the *track*, but
   without this the *item* still refuses to shrink below its content's
   intrinsic width and overflows the track anyway. A long unbroken email
   address was rendering past its column and into the next one until this
   was added alongside the flex-level fix - the two are independent, this
   is not a duplicate of that one. */
.form-grid .field { min-width: 0; }

.form-grid .field--wide { grid-column: 1 / -1; }

/* min-width:0 here is the third layer of the same trap: .field__row is
   itself a grid item of .field's own (implicit, auto-sized) single-column
   grid, so - independent of .field's own width being correctly constrained
   by *its* parent grid - .field__row was free to size to its own preferred
   (content) width and overflow straight out of .field's box. Three
   independent min-width:auto defaults (this one, the grid .field one, and
   the flex .input one below) all had to be overridden for one truncating
   value + button pair to actually stay inside its column. */
.field__row { display: flex; gap: 0.5rem; align-items: center; min-width: 0; }
/* min-width:0 is load-bearing: a flex item's default min-width is auto,
   which for a long unbroken mono string (an email/password with no spaces)
   is its full intrinsic width - without this the input refuses to shrink
   for the copy button and overflows the grid cell into whatever sits next
   to it, instead of the ellipsis truncation .field__row__value asks for. */
.field__row .input { flex: 1; min-width: 0; }

.field__row__value {
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* One bordered box standing in for two fields: the local-part input, a
   literal "@", and the domain <select>, flush against each other so the
   group reads as "typing address@domain" rather than three unrelated
   controls that happen to sit in a row. The focus/hover treatment lives
   on this wrapper (border-color, background, glow) instead of on the
   inner input/select, which are both stripped of their own border/
   background below so nothing doubles up. min-width:0 because .addr-combo
   is itself a grid item of .field's own (implicit, single-column) grid -
   the same min-width:auto trap .field__row documents further down: without
   this the combo refuses to shrink below its content's intrinsic width
   (now wider than before, thanks to the domain select's flat 160px) and
   overflows its column at the 2-up tablet breakpoint instead of letting
   the address input's own min-width:0 do its job. */
.addr-combo {
  display: flex;
  align-items: stretch;
  min-width: 0;
  border: 1px solid var(--line-mid);
  border-radius: var(--radius);
  background: var(--wash-3);
  transition: border-color 0.25s, box-shadow 0.25s, background 0.25s;
}

.addr-combo:hover { border-color: var(--line-strong); }

.addr-combo:focus-within {
  border-color: var(--accent-deep);
  background: var(--wash-1);
  box-shadow: 0 0 0 3px var(--accent-wash);
}

/* Right-aligned so a short value sits flush against the @ instead of
   drifting left with a gap before it. */
.addr-combo__local {
  flex: 1 1 auto;
  min-width: 0;
  width: auto;
  border: 0;
  background: none;
  padding: 0.85rem 0 0.85rem 1rem;
  color: var(--fg-strong);
  font-family: var(--font-mono);
  font-size: 0.92rem;
  letter-spacing: 0.02em;
  text-align: right;
}

.addr-combo__local::placeholder { color: var(--fg-faint); letter-spacing: 0.08em; }
.addr-combo__local:focus { outline: none; }

.addr-combo__at {
  flex: none;
  display: flex;
  align-items: center;
  padding-block: 0.85rem;
  color: var(--fg-faint);
  font-family: var(--font-mono);
  font-size: 0.92rem;
}

/* The domain <select> keeps its own class (still styled by .domain-select
   below for the chevron/typography) but sheds its border and fill here so
   it reads as the tail end of .addr-combo, not a second boxed control.
   width is a flat value on purpose, not auto: a select with
   appearance:none and width:auto never paints its background-image at
   all in Chromium (confirmed - the chevron silently vanishes, no error,
   nothing else about the element looks wrong), so this can't just shrink
   to content like the input beside it. Same "flat, not a range" reasoning
   the original standalone .domain-select used - it holds a fixed set of
   known values (config.MAIL_DOMAINS), sized to fit the longest one
   ("og-society.com") plus the chevron. */
.addr-combo .domain-select {
  flex: none;
  width: 160px;
  border: 0;
  background-color: transparent;
  padding-left: 0.15rem;
}

.addr-combo .domain-select:focus { box-shadow: none; }

.domain-select {
  width: 100%;
  padding: 0.85rem 1rem;
  border: 1px solid var(--line-mid);
  border-radius: var(--radius);
  background: var(--wash-3);
  color: var(--fg-strong);
  font-family: var(--font-mono);
  font-size: 0.92rem;
  appearance: none;
  /* An embedded data-URI can't reference a CSS variable, so the chevron's
     stroke is a baked-in copy of --fg-dim - kept in sync by hand (see the
     light-theme override just below, which re-encodes the same path with
     the light palette's --fg-dim instead). */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%2391a0b4' stroke-width='1.4' fill='none'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 1rem center;
}

:root[data-theme="light"] .domain-select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23566373' stroke-width='1.4' fill='none'/%3E%3C/svg%3E");
}

.domain-select:focus { outline: none; border-color: var(--accent-deep); box-shadow: 0 0 0 3px var(--accent-wash); }

/* ------------------------------------------------------------------ inbox */
.inbox { display: grid; gap: 0.6rem; }

.inbox__row {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-glass);
  overflow: hidden;
}

.inbox__head-text { min-width: 0; }

.inbox__head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  padding: 0.85rem 1.05rem;
  background: none;
  border: none;
  text-align: left;
  cursor: pointer;
  color: inherit;
}

/* Sender+date read first (who is this actually from, and when - the two
   things worth trusting before you trust the body), subject second. Sender
   carries the visual weight; a raw address is not a punchy proper noun the
   way a subject line is, so it needs the extra size/contrast just to read
   as the primary line rather than the caption under it. */
.inbox__from {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--fg-strong);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.inbox__date {
  flex: none;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  color: var(--fg-dim);
}
.inbox__subject { color: var(--fg-dim); font-weight: 400; margin: 0.15rem 0 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.inbox__meta { margin: 0; display: flex; align-items: baseline; justify-content: space-between; gap: 0.6rem; }
.inbox__snippet { margin: 0.3rem 0 0; font-size: 0.83rem; color: var(--fg-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.inbox__chev { color: var(--fg-faint); transition: transform 0.25s var(--ease); flex: none; }
.inbox__row.is-open .inbox__chev { transform: rotate(90deg); color: var(--accent); }

.inbox__body { display: none; border-top: 1px solid var(--line); padding: 0.9rem; }
.inbox__row.is-open .inbox__body { display: grid; gap: 0.7rem; }

.inbox__toolbar { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }

/* A sandboxed iframe with neither allow-same-origin nor allow-scripts (both
   deliberately withheld - see main.py's api_message_render) cannot report
   its own content height to this page: there is no postMessage without
   scripts, and reading contentDocument directly is exactly what the opaque
   origin blocks. So this cannot auto-size to content the way a normal
   block element would - a short email otherwise renders as acres of blank
   white. A fixed, human-sized default plus its own scrollbar for anything
   longer, and a drag handle for anyone who wants more room, beats either
   guessing at content height or a viewport-relative height that balloons
   on a tall monitor regardless of what is actually in the message. */
.msgframe {
  width: 100%;
  height: 340px;
  border: 1px solid var(--line-mid);
  border-radius: var(--radius);
  /* Matches mailsanitize._wrap_document's own background so there is no
     flash of anything else before the document loads - the rendered
     page's <html> background already fills the iframe regardless, this
     is just the frame's own resting color for that instant. */
  background: var(--ink-100);
  resize: vertical;
  overflow: auto;
}

/* A plain-text body (see den-login.js and mailsanitize.render_text_as_html)
   is typically a handful of lines, not a full HTML layout - the 340px
   default above was drawing a lot of card for very little content. Same
   resize handle still covers the rare long one. */
.msgframe--plain { height: 160px; }

/* ------------------------------------------------------------------ table */
.table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); }
.table-wrap--spaced { margin-bottom: 2.25rem; }

.rtable { width: 100%; border-collapse: collapse; font-size: 0.88rem; min-width: 560px; }

.rtable th {
  text-align: left;
  padding: 0.7rem 1rem;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg-dim);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

.rtable td {
  padding: 0.75rem 1rem;
  border-bottom: 1px dashed var(--line-soft);
  color: var(--fg);
  vertical-align: middle;
}

.rtable tr:last-child td { border-bottom: none; }
.rtable td.is-mono { font-family: var(--font-mono); font-size: 0.85rem; }

.roster-expand__chev { color: var(--fg-faint); transition: transform 0.25s var(--ease); }
.roster-expand.is-open .roster-expand__chev { transform: rotate(90deg); color: var(--accent); }

.roster-detail-row td { padding: 0 1rem 0.9rem; border-bottom: 1px dashed var(--line-soft); }

.roster-detail { display: grid; gap: 0.5rem; }

.roster-detail__row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem 0.9rem;
  padding: 0.55rem 0.8rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-soft);
  font-size: 0.85rem;
}

.roster-detail__game { color: var(--fg-strong); font-weight: 550; }
.roster-detail__game:hover { color: var(--accent); text-decoration: underline; }
.roster-detail__email { font-family: var(--font-mono); color: var(--fg-dim); }
.roster-detail__meta { margin-left: auto; font-family: var(--font-mono); font-size: 0.8125rem; color: var(--fg-dim); }

.clearance-select {
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--line-mid);
  border-radius: var(--radius);
  background: var(--wash-3);
  color: var(--fg-strong);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
}

/* ------------------------------------------------------------- invite mint */
.mint-result {
  display: none;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: 0.9rem;
  padding: 0.8rem 1rem;
  border: 1px solid var(--line-hot);
  border-radius: var(--radius);
  background: var(--accent-wash);
  font-family: var(--font-mono);
  font-size: 0.85rem;
  letter-spacing: 0.08em;
  color: var(--fg-strong);
}

.mint-result.is-shown { display: flex; }

@media (max-width: 720px) {
  .vault__head { display: none; }
  .vault__row {
    grid-template-columns: 1fr;
    gap: 0.5rem;
    align-items: start;
  }
  .vault__label { display: block; }
  .vault__actions { justify-content: flex-end; width: 100%; }
  .denbar__inner { flex-wrap: wrap; height: auto; padding-block: 0.75rem; }
  .userbar { order: 3; width: 100%; justify-content: space-between; }
}
