/* ============================================================
   10X CRM — Components
   Window chrome, buttons, inputs, badges, chips, cards, modals,
   toasts, navigation, stats and the pipeline bar.
   Depends on tokens.css + base.css.
   ============================================================ */

/* ============================================================
   1. WINDOW CHROME — the signature component
   Every panel, card and modal in the app is a "window": a bevelled
   box with a gradient title bar and Win2K control buttons. This is
   what makes the aesthetic structural rather than decorative.
   ============================================================ */
.window {
  position: relative;
  /* Translucent glass rather than a solid fill, so the haze and the
     rain are visible THROUGH the panel. That is what ties the interface
     to its background instead of leaving it floating on top of it.
     --glass is only 72% opaque but still dark enough to keep body text
     well clear of the contrast floor. */
  background-image: var(--gloss-face), linear-gradient(var(--glass), var(--glass));
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--chrome-edge);
  border-radius: var(--r-4);
  box-shadow: var(--lift);
}

/* The title bar is the loudest thing in the app, so it earns the
   iridescent ramp: a CD-surface sheen panning slowly back and forth
   under a fixed specular glare. `alternate` is doing real work here —
   panning in one direction would snap at the loop, and reversing hides
   the seam completely without needing the gradient to tile. */
.window__bar {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-2) var(--sp-2) var(--sp-3);

  background-image: var(--gloss-spec), var(--iri);
  background-size: 100% 100%, 300% 100%;
  background-position: 0 0, 0% 0;
  animation: iri-pan 14s linear infinite alternate;

  /* Matches the window's own radius so the bar's corners sit inside the
     rounded frame. Avoids overflow:hidden on .window, which would clip
     the detail panel's scrolling content. */
  border-radius: calc(var(--r-4) - 1px) calc(var(--r-4) - 1px) 0 0;
  border-bottom: 1px solid var(--chrome-edge);
  color: var(--on-accent);
  font-family: var(--font-display);
  font-size: var(--fs-display-sm);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.45);
}

@keyframes iri-pan {
  to { background-position: 0 0, 100% 0; }
}

.window__title {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.window__controls {
  display: flex;
  gap: var(--sp-1);
}

/* Visually these are small Win2K-sized boxes (22x18). The ::before
   pseudo-element expands the actual clickable area out to the 44px
   accessibility floor without changing how they look. */
.window__btn {
  position: relative;
  width: 22px;
  height: 18px;
  display: grid;
  place-items: center;
  background: var(--surface);
  border: 1px solid var(--bevel-dark);
  box-shadow: var(--bevel-out);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  line-height: 1;
}

.window__btn::before {
  content: "";
  position: absolute;
  inset: -13px;
}

.window__btn:active {
  box-shadow: var(--bevel-in);
}

.window__btn--close:hover {
  background: var(--danger);
  color: var(--on-accent);
}

.window__body {
  padding: var(--sp-5);
}

/* ============================================================
   2. BUTTONS — physical arcade buttons
   On :active the button translates 2px down-right and its hard shadow
   collapses to zero, so the surface appears to sink into the page.
   ============================================================ */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: var(--tap-min);
  padding: var(--sp-3) var(--sp-5);

  font-family: var(--font-display);
  font-size: var(--fs-display-sm);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  text-decoration: none;

  color: var(--text);
  /* Gloss over the fill, not instead of it: --surface-up still decides
     what colour the button is, the gradient only adds the moulding. */
  background-image: var(--gloss-face), linear-gradient(var(--surface-up), var(--surface-up));
  border: 1px solid var(--chrome-edge);
  border-radius: var(--r-pill);
  box-shadow: var(--shadow-hard-sm), inset 0 1px 0 rgba(255, 255, 255, 0.22);

  transition: transform var(--t-snap), box-shadow var(--t-fast),
              filter var(--t-fast), color var(--t-fast);
}

/* The specular glare across the top half — the single detail that makes
   a shape read as moulded plastic rather than a coloured rectangle.
   inset:1px keeps it inside the border; `auto` on the bottom lets the
   height rule size it. */
.btn::after {
  content: "";
  position: absolute;
  inset: 1px 1px auto 1px;
  height: 46%;
  border-radius: var(--r-pill) var(--r-pill) 40% 40% / var(--r-pill) var(--r-pill) 100% 100%;
  background: var(--gloss-spec);
  pointer-events: none;
}

.btn:hover {
  filter: brightness(1.12);
  box-shadow: var(--shadow-hard-sm), var(--bloom-md) var(--accent);
}

/* Kept exactly as it was: the button sinks 2px and its shadow collapses
   to nothing, so it reads as physically pressed. Gloss did not replace
   this — the arcade feel was the one part of the old design that worked. */
.btn:active {
  transform: translate(2px, 2px);
  box-shadow: 0 0 0 var(--bevel-dark);
}

.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
  filter: none;
  box-shadow: var(--shadow-hard-sm);
}

.btn:disabled::after { opacity: 0.3; }

.btn--primary {
  background-image: var(--gloss-face), linear-gradient(var(--accent), var(--accent));
  color: var(--on-accent);
  border-color: var(--accent);
  box-shadow: var(--shadow-hard-sm), var(--bloom-sm) var(--accent),
              inset 0 1px 0 rgba(255, 255, 255, 0.4);
}

.btn--primary:hover {
  filter: brightness(1.15);
  box-shadow: var(--shadow-hard-sm), var(--bloom-lg) var(--accent);
}

.btn--danger {
  background-image: var(--gloss-face), linear-gradient(transparent, transparent);
  color: var(--danger);
  border-color: var(--danger);
}

.btn--danger:hover {
  background-image: var(--gloss-face), linear-gradient(var(--danger), var(--danger));
  color: var(--on-accent);
  box-shadow: var(--shadow-hard-sm), var(--bloom-md) var(--danger);
}

.btn--ghost {
  background-image: none;
  background-color: transparent;
  border-color: var(--border);
  box-shadow: none;
}

.btn--ghost::after { content: none; }

.btn--ghost:hover {
  border-color: var(--accent);
  box-shadow: var(--bloom-sm) var(--accent);
}

.btn--ghost:active { transform: translate(1px, 1px); }

/* A <label> carrying .btn is the file-upload trigger. It gets every button
   style for free, but not the cursor, because base.css sets that on <button>
   only and a label is not one. */
label.btn { cursor: pointer; }

/* The hidden file input is still focusable, so its focus ring has to land on
   something visible — otherwise a keyboard user tabs onto an invisible control
   with no feedback at all. :focus-within puts the ring on the label instead. */
.sr-only:focus-visible + label.btn {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  box-shadow: var(--bloom-md) var(--accent);
}

.btn--block { width: 100%; }

.btn--sm {
  min-height: 32px;
  padding: var(--sp-2) var(--sp-3);
}

/* ============================================================
   3. FORM FIELDS
   Inputs use the INVERSE bevel of a button: they are sunk into the
   page rather than raised out of it. That single inversion is what
   makes an input read as "type here" and a button as "press me".
   ============================================================ */
.field {
  display: grid;
  gap: var(--sp-2);
}

.field__label {
  font-family: var(--font-display);
  font-size: var(--fs-display-sm);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
}

.input,
.select {
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--sp-3);
  font-family: var(--font-ui);
  font-size: var(--fs-base);
  color: var(--text);
  /* A solid well, not glass. Everything else in the app went
     translucent; the one place that must not is the surface a user
     reads their own typing off. */
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: var(--r-3);
  box-shadow: var(--bevel-in), inset 0 2px 6px rgba(0, 0, 0, 0.35);
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
}

.input::placeholder { color: var(--text-faint); }

/* Focus is now a bloom rather than a flat ring: the field lights up
   from inside. The 2px solid ring underneath survives for anyone who
   cannot perceive the glow. */
.input:focus-visible,
.select:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--bevel-in), var(--focus-bloom);
}

.select {
  cursor: pointer;
  appearance: none;
  padding-right: var(--sp-6);
  /* Pixel-style caret drawn with two crossed gradients — no icon file. */
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:
    calc(100% - 18px) calc(50% + 2px),
    calc(100% - 12px) calc(50% + 2px);
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
}

/* Error state (P0.4): red border on the field itself, red message below. */
.input-error,
.input-error:focus-visible {
  border-color: var(--danger);
}

.field__error {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-1);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  line-height: 1.4;
  color: var(--danger);
}

.field__error::before {
  content: "\25B8";  /* ▸ */
  flex-shrink: 0;
}

.field__error:empty { display: none; }

/* ============================================================
   4. STATUS BADGES
   A badge is a hard-edged pixel dot plus an uppercase text label.
   The dot is never the only signal — the label always states the
   status in words, so the badge works without colour perception.
   ============================================================ */
.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2);
  font-family: var(--font-display);
  font-size: var(--fs-display-sm);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  border: 2px solid currentColor;
  background: color-mix(in srgb, currentColor 12%, transparent);
}

/* The dot is the one place the pixel layer stays literal: a hard 6px
   square, no radius. It breathes on a slow loop so a screen full of
   badges has a faint life to it rather than sitting dead. */
.badge::before {
  content: "";
  width: 6px;
  height: 6px;
  flex-shrink: 0;
  background: currentColor;
  box-shadow: 0 0 var(--glow-strength) currentColor;
  animation: dot-pulse var(--t-pulse) ease-in-out infinite;
}

@keyframes dot-pulse {
  0%, 100% { box-shadow: 0 0 var(--glow-strength) currentColor; opacity: 1; }
  50%      { box-shadow: 0 0 calc(var(--glow-strength) * 2.2) currentColor; opacity: 0.75; }
}

.badge--lead      { color: var(--status-lead); }
.badge--contacted { color: var(--status-contacted); }
.badge--won       { color: var(--status-won); }
.badge--lost      { color: var(--status-lost); }

/* ============================================================
   5. FILTER CHIPS
   A pressed chip flips from raised bevel to inset bevel and fills
   with the accent, so "which filter is active" is obvious from the
   shape alone as well as the colour.
   ============================================================ */
.chip {
  position: relative;
  min-height: var(--tap-min);
  padding: var(--sp-2) var(--sp-5);
  font-family: var(--font-display);
  font-size: var(--fs-display-sm);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-dim);
  background-image: var(--gloss-face), linear-gradient(var(--surface), var(--surface));
  border: 1px solid var(--chrome-edge);
  border-radius: var(--r-pill);
  box-shadow: var(--bevel-out);
  transition: color var(--t-fast), box-shadow var(--t-fast), filter var(--t-fast);
}

.chip::after {
  content: "";
  position: absolute;
  inset: 1px 1px auto 1px;
  height: 46%;
  border-radius: var(--r-pill) var(--r-pill) 40% 40% / var(--r-pill) var(--r-pill) 100% 100%;
  background: var(--gloss-spec);
  pointer-events: none;
}

.chip:hover {
  color: var(--text);
  filter: brightness(1.12);
  box-shadow: var(--bevel-out), var(--bloom-sm) var(--accent);
}

/* A pressed chip fills with the accent AND keeps the inset bevel, so
   "which filter is on" is readable from the shape as well as the
   colour — the same guarantee the badges make. */
.chip[aria-pressed="true"] {
  color: var(--on-accent);
  background-image: var(--gloss-face), linear-gradient(var(--accent), var(--accent));
  border-color: var(--accent);
  box-shadow: var(--bevel-in), var(--bloom-md) var(--accent);
}

/* ============================================================
   6. AVATAR
   Either a real API image or generated initials on a hashed accent.
   image-rendering: pixelated keeps remote avatars consistent with the
   sprite aesthetic instead of smoothing them.
   ============================================================ */
.avatar {
  width: 48px;
  height: 48px;
  flex-shrink: 0;
  object-fit: cover;
  background: var(--bg-deep);
  border: 1px solid var(--chrome-edge);
  border-radius: var(--r-3);
  box-shadow: var(--bloom-sm) var(--accent), var(--bevel-out);
  image-rendering: pixelated;
}

.avatar--initials {
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-size: var(--fs-display-sm);
  color: var(--on-accent);
  background-image: var(--gloss-face), linear-gradient(var(--accent), var(--accent));
}

.avatar--lg { width: 72px; height: 72px; font-size: var(--fs-display); }

/* ============================================================
   7. CLIENT CARD
   Lifts up-left on hover (shadow grows to compensate) and a single
   bright scanline sweeps down the card once.
   ============================================================ */
.client-card {
  position: relative;
  overflow: hidden;
  display: grid;
  grid-template-columns: 48px 1fr auto;
  gap: var(--sp-4);
  align-items: center;
  padding: var(--sp-4);
  /* NO backdrop-filter here, deliberately — see the note on --glass-solid
     in tokens.css. There are thirty of these on the clients page, and
     thirty live blur layers over a moving background is the single most
     expensive thing this app was doing. The fill is opaque enough that
     the difference is not visible; the cost difference is enormous. */
  background-image: var(--gloss-face), linear-gradient(var(--glass-solid), var(--glass-solid));
  border: 1px solid var(--chrome-edge);
  border-radius: var(--r-3);
  box-shadow: var(--shadow-hard-sm);
  cursor: pointer;
  transition: transform var(--t-snap), box-shadow var(--t-fast),
              border-color var(--t-fast);
}

/* A thin iridescent seam down the leading edge. Hidden until hover, so
   a list of thirty cards is not thirty rainbows — it only lights up on
   the one you are pointing at.

   The ANIMATION is on the hover state too, not just the opacity. It used
   to run unconditionally: thirty cards each repainting a scrolling
   gradient, forever, all of them invisible at opacity 0. Being unable to
   see an animation does not stop the browser drawing it. */
.client-card::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 3px;
  background-image: var(--iri);
  background-size: 100% 300%;
  opacity: 0;
  transition: opacity var(--t-fast);
}

@keyframes iri-slide {
  to { background-position: 0 300%; }
}

.client-card:hover {
  transform: translate(-2px, -2px);
  border-color: var(--accent);
  box-shadow: var(--shadow-hard), var(--bloom-md) var(--accent);
}

.client-card:hover::before {
  opacity: 1;
  animation: iri-slide 6s linear infinite;
}

.client-card::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: -2px;
  height: 2px;
  background: var(--accent);
  opacity: 0;
  pointer-events: none;
}

.client-card:hover::after {
  animation: sweep 600ms steps(12, end) 1;
}

@keyframes sweep {
  from { top: -2px;  opacity: 0.9; }
  to   { top: 100%;  opacity: 0; }
}

.client-card__name {
  font-family: var(--font-ui);
  font-size: var(--fs-lg);
  font-weight: 500;
  line-height: var(--lh-tight);
}

.client-card__meta {
  font-size: var(--fs-sm);
  color: var(--text-dim);
}

.client-card__value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-lg);
  color: var(--success);
}

.client-card__actions {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

/* ============================================================
   8. MODAL / DIALOG
   The overlay dims and desaturates the page behind. The window itself
   opens with a CRT power-on: a flat horizontal line that snaps open
   vertically.
   ============================================================ */
.overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: grid;
  place-items: center;
  padding: var(--sp-4);
  background: color-mix(in srgb, var(--bg-void) 82%, transparent);
  backdrop-filter: blur(2px) saturate(0.6);
  animation: fade-in var(--t-fast) both;
}

.overlay[hidden] { display: none; }

.modal {
  width: 100%;
  max-width: 520px;
  max-height: 90vh;
  overflow: auto;
  box-shadow: var(--lift), var(--bloom-lg) var(--accent);
  animation: crt-on 320ms steps(8, end) both;
}

.modal--wide { max-width: 640px; }

/* A CRT switching on: the picture arrives as a blown-out horizontal
   line, snaps open vertically, overshoots, then settles. steps() is
   the whole trick — advancing in discrete frames reads as hardware
   waking up, where a smooth curve would just read as a fade. */
@keyframes crt-on {
  0%   { transform: scaleY(0.004) scaleX(0.7); filter: brightness(6) saturate(0); opacity: 1; }
  35%  { transform: scaleY(0.02) scaleX(1.02); filter: brightness(4) saturate(0.4); }
  55%  { transform: scaleY(1.06) scaleX(1);    filter: brightness(2.2); }
  75%  { transform: scaleY(0.98) scaleX(1);    filter: brightness(1.4); }
  100% { transform: none;                      filter: none; }
}

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

/* ============================================================
   9. TOASTS (P0.4)
   Stacked bottom-right. The draining progress bar is a visual mirror
   of the 3s setTimeout that removes the toast, so the countdown the
   code is running is actually visible on screen.
   ============================================================ */
.toast-stack {
  position: fixed;
  right: var(--sp-4);
  bottom: var(--sp-4);
  z-index: 2000;
  display: grid;
  gap: var(--sp-2);
  justify-items: end;
  pointer-events: none;
}

.toast {
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-width: 280px;
  max-width: 380px;
  padding: var(--sp-3) var(--sp-4);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--text);
  background-image: var(--gloss-face), linear-gradient(var(--glass), var(--glass));
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border: 1px solid var(--chrome-edge);
  border-left-width: 3px;
  border-radius: var(--r-3);
  box-shadow: var(--lift);
  pointer-events: auto;
  animation: toast-in 200ms steps(4, end) both;
}

/* Each toast blooms in its own colour, so what kind of message it is
   registers from the corner of your eye before you read a word. */
.toast--success {
  border-color: var(--success);
  box-shadow: var(--lift), var(--bloom-md) var(--success);
}

.toast--error {
  border-color: var(--danger);
  box-shadow: var(--lift), var(--bloom-md) var(--danger);
}

.toast--info {
  border-color: var(--accent);
  box-shadow: var(--lift), var(--bloom-md) var(--accent);
}

.toast__icon {
  font-family: var(--font-display);
  font-size: var(--fs-display-sm);
  flex-shrink: 0;
}

.toast--success .toast__icon { color: var(--success); }
.toast--error   .toast__icon { color: var(--danger); }
.toast--info    .toast__icon { color: var(--accent); }

.toast__msg { flex: 1; }

.toast__close {
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  color: var(--text-dim);
  background: transparent;
  border: 1px solid var(--border);
}

.toast__close:hover { color: var(--text); border-color: var(--text); }

.toast__bar {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 3px;
  background: currentColor;
  animation: drain var(--toast-duration, 3000ms) linear forwards;
}

.toast--success .toast__bar { background: var(--success); }
.toast--error   .toast__bar { background: var(--danger); }
.toast--info    .toast__bar { background: var(--accent); }

@keyframes drain {
  from { width: 100%; }
  to   { width: 0%; }
}

@keyframes toast-in {
  from { transform: translateX(28px); opacity: 0; }
  to   { transform: none; opacity: 1; }
}

/* ============================================================
   10. TASKBAR NAVIGATION (P0.2)
   Identical on all three protected pages, rendered from one module.
   ============================================================ */
.taskbar {
  position: sticky;
  top: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  background-image: var(--gloss-face), linear-gradient(var(--glass), var(--glass));
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  border-bottom: 1px solid var(--chrome-edge);
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5), inset 0 1px 0 var(--chrome-edge);
}

/* A hairline of iridescence along the bottom edge of the bar, panning
   continuously. It is the one piece of ambient motion that is on every
   protected page, which is what makes the whole app feel powered on. */
.taskbar::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 1px;
  background-image: var(--iri);
  background-size: 300% 100%;
  animation: iri-pan 10s linear infinite alternate;
}

.taskbar__logo {
  font-family: var(--font-display);
  font-size: var(--fs-display);
  color: var(--accent);
  text-decoration: none;
  padding: var(--sp-2) var(--sp-3);
  margin-right: var(--sp-3);
  text-shadow: 0 0 14px var(--accent);
}

/* Two offset colour copies jittering on steps() = chromatic glitch. */
.taskbar__logo:hover {
  animation: glitch 320ms steps(4, end) infinite;
}

@keyframes glitch {
  0%   { text-shadow:  2px 0 var(--danger), -2px 0 var(--accent); }
  33%  { text-shadow: -2px 0 var(--danger),  2px 0 var(--accent); }
  66%  { text-shadow:  1px 0 var(--accent), -1px 0 var(--danger); }
  100% { text-shadow: -1px 0 var(--accent),  1px 0 var(--danger); }
}

.nav-link {
  min-height: 36px;
  display: inline-flex;
  align-items: center;
  padding: var(--sp-2) var(--sp-4);
  font-family: var(--font-display);
  font-size: var(--fs-display-sm);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  text-decoration: none;
  color: var(--text-dim);
  background-image: var(--gloss-face), linear-gradient(var(--surface), var(--surface));
  border: 1px solid var(--chrome-edge);
  border-radius: var(--r-pill);
  box-shadow: var(--bevel-out);
  transition: color var(--t-fast), box-shadow var(--t-fast), filter var(--t-fast);
}

.nav-link:hover {
  color: var(--text);
  filter: brightness(1.15);
  box-shadow: var(--bevel-out), var(--bloom-sm) var(--accent);
}

/* Active page: filled, inset AND blooming, so it reads as a pressed
   taskbar tab from the shape alone as well as the colour. */
.nav-link.active {
  color: var(--on-accent);
  background-image: var(--gloss-face), linear-gradient(var(--accent), var(--accent));
  border-color: var(--accent);
  box-shadow: var(--bevel-in), var(--bloom-md) var(--accent);
}

.taskbar__spacer { flex: 1; }

.icon-btn {
  min-width: var(--tap-min);
  min-height: 36px;
  display: grid;
  place-items: center;
  padding: var(--sp-2) var(--sp-3);
  font-family: var(--font-display);
  font-size: var(--fs-display-sm);
  color: var(--text);
  background-image: var(--gloss-face), linear-gradient(var(--surface), var(--surface));
  border: 1px solid var(--chrome-edge);
  border-radius: var(--r-pill);
  box-shadow: var(--bevel-out);
  transition: color var(--t-fast), box-shadow var(--t-fast);
}

.icon-btn:hover {
  color: var(--accent);
  border-color: var(--accent);
  box-shadow: var(--bevel-out), var(--bloom-md) var(--accent);
}

.icon-btn:active { box-shadow: var(--bevel-in); }

/* ============================================================
   11. STAT CARDS (P3.2)
   ============================================================ */
/* A stat card is an instrument readout: a dark recessed LCD well set
   into a glossy chrome bezel, with the number glowing off it. */
.stat {
  position: relative;
  overflow: hidden;
  padding: var(--sp-4);
  /* Opaque fill rather than a live blur, for the same reason as
     .client-card — there are four to six of these on a page, sitting
     directly under the sweep. */
  background-image: var(--gloss-face), linear-gradient(var(--glass-solid), var(--glass-solid));
  border: 1px solid var(--chrome-edge);
  border-radius: var(--r-3);
  box-shadow: var(--lift);
  transition: box-shadow var(--t-fast), transform var(--t-snap);
}

.stat:hover {
  transform: translateY(-2px);
  box-shadow: var(--lift), var(--bloom-md) var(--accent);
}

/* Corner bracket — the HUD cue that says "this is an instrument". */
.stat::before {
  content: "";
  position: absolute;
  top: 6px;
  right: 6px;
  width: 12px;
  height: 12px;
  border-top: 2px solid var(--accent);
  border-right: 2px solid var(--accent);
  opacity: 0.7;
}

.stat__label {
  font-family: var(--font-display);
  font-size: var(--fs-display-sm);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
}

.stat__value {
  margin-top: var(--sp-3);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-2xl);
  line-height: 1;
  color: var(--text);
  text-shadow: 0 0 20px currentColor;
}

.stat--accent .stat__value  { color: var(--accent); }
.stat--success .stat__value { color: var(--success); }
.stat--warning .stat__value { color: var(--warning); }

/* ============================================================
   12. PIPELINE BAR (P3.3 + the "mini chart" bonus in one component)
   Hard-edged segments sized in proportion to each status count.
   ============================================================ */
.pipeline {
  display: flex;
  height: 36px;
  border: 1px solid var(--chrome-edge);
  border-radius: var(--r-pill);
  box-shadow: var(--bevel-in), inset 0 2px 8px rgba(0, 0, 0, 0.5);
  background: var(--bg-deep);
  overflow: hidden;
}

/* Each segment is a lit bar behind glass: its own colour underneath,
   the gloss gradient over it, and a bloom in its own colour spilling
   sideways onto its neighbours. */
.pipeline__seg {
  position: relative;
  display: grid;
  place-items: center;
  min-width: 0;
  font-family: var(--font-display);
  font-size: var(--fs-display-sm);
  color: var(--on-accent);
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5);
  animation: seg-grow 500ms steps(8, end) both;
  transform-origin: left center;
}

.pipeline__seg::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 50%;
  background: var(--gloss-spec);
  pointer-events: none;
}

.pipeline__seg--lead {
  background: var(--status-lead);
  box-shadow: var(--bloom-md) var(--status-lead);
}

.pipeline__seg--contacted {
  background: var(--status-contacted);
  box-shadow: var(--bloom-md) var(--status-contacted);
}

.pipeline__seg--won {
  background: var(--status-won);
  box-shadow: var(--bloom-md) var(--status-won);
}

.pipeline__seg--lost {
  background: var(--status-lost);
  box-shadow: var(--bloom-md) var(--status-lost);
}

@keyframes seg-grow {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* Legend below the bar — the numbers in words, so the chart is not
   colour-only. */
.pipeline-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  margin-top: var(--sp-3);
}

.pipeline-legend__item {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--text-dim);
}

.pipeline-legend__item::before {
  content: "";
  width: 8px;
  height: 8px;
  background: currentColor;
}

.pipeline-legend__item--lead      { color: var(--status-lead); }
.pipeline-legend__item--contacted { color: var(--status-contacted); }
.pipeline-legend__item--won       { color: var(--status-won); }
.pipeline-legend__item--lost      { color: var(--status-lost); }

/* ============================================================
   13. STATES — loading, empty, error (P4.2 / P4.3)
   ============================================================ */
.state {
  display: grid;
  place-items: center;
  gap: var(--sp-4);
  padding: var(--sp-8) var(--sp-4);
  text-align: center;
  border: 2px dashed var(--border);
  background: color-mix(in srgb, var(--surface) 50%, transparent);
}

.state__msg {
  font-family: var(--font-display);
  font-size: var(--fs-display-sm);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
  line-height: 1.8;
}

.state--error { border-color: var(--danger); }
.state--error .state__msg { color: var(--danger); }

/* Blinking block cursor after "Loading clients..." */
.cursor::after {
  content: "\2588";  /* █ */
  color: var(--accent);
  animation: blink 1s steps(2, jump-none) infinite;
}

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

/* Eight-dot pixel spinner built entirely from box-shadows, stepping
   one position at a time so it ticks like a sprite. */
.spinner {
  width: 6px;
  height: 6px;
  background: var(--accent);
  box-shadow:
     14px   0   0 color-mix(in srgb, var(--accent) 80%, transparent),
     10px  10px 0 color-mix(in srgb, var(--accent) 65%, transparent),
      0    14px 0 color-mix(in srgb, var(--accent) 50%, transparent),
    -10px  10px 0 color-mix(in srgb, var(--accent) 40%, transparent),
    -14px   0   0 color-mix(in srgb, var(--accent) 30%, transparent),
    -10px -10px 0 color-mix(in srgb, var(--accent) 20%, transparent),
      0   -14px 0 color-mix(in srgb, var(--accent) 15%, transparent),
     10px -10px 0 color-mix(in srgb, var(--accent) 10%, transparent);
  animation: spin 800ms steps(8, end) infinite;
}

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

/* ============================================================
   14. NOTES TIMELINE (P4.8)
   ============================================================ */
.note-list {
  display: grid;
  gap: var(--sp-2);
  max-height: 220px;
  overflow-y: auto;
  padding: var(--sp-3);
  background: var(--bg-deep);
  border: 2px solid var(--border);
  box-shadow: var(--bevel-in);
}

.note {
  display: grid;
  gap: var(--sp-1);
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface);
  border-left: 3px solid var(--accent);
}

.note__text { font-size: var(--fs-sm); }

.note__date {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--text-faint);
}

/* ============================================================
   15. CRT MODE — the Konami code easter egg
   Turns the faint ambient scanlines into a full phosphor CRT:
   heavy lines, a slow flicker, and a green glow on all text.
   ============================================================ */
/* The whole app becomes a green phosphor terminal.

   This is the payoff for putting every colour in a token: the class
   does not restyle a single component. It redefines the variables on
   <body>, and because every component derives its colour from those
   variables rather than hardcoding one, the entire interface — cards,
   chrome, badges, pipeline, toasts, the assistant — turns green at
   once. Roughly twenty lines here replace what would otherwise be a
   second stylesheet.

   Note the class lands on <body>, not <html>, so custom properties
   inherit down to everything inside including body::before.

   SCOPED TO THE DARK THEME. CRT mode is the dark theme's easter egg — a
   phosphor tube belongs on a black screen, not a pale one — and the light
   theme has its own (the breach, section 19.7). Scoping it here means the two
   can never render at the same time: each theme's saved egg flag can be on
   independently, but only the one matching the current theme is ever drawn, so
   they cannot stack. The default theme always carries data-theme="dark", so
   this matches whenever the light theme is not active. */
[data-theme="dark"] body.crt-mode {
  --text: var(--phosphor);
  --text-dim: color-mix(in srgb, var(--phosphor) 65%, transparent);
  --text-faint: color-mix(in srgb, var(--phosphor) 40%, transparent);

  --accent: var(--phosphor);
  --violet: var(--phosphor);
  --cyan: var(--phosphor);
  --lime: var(--phosphor);
  --amber: var(--phosphor);
  --magenta: var(--phosphor);

  --status-lead: var(--phosphor);
  --status-contacted: var(--phosphor);
  --status-won: var(--phosphor);
  --status-lost: var(--phosphor);

  --border: color-mix(in srgb, var(--phosphor) 45%, transparent);
  --chrome-edge: color-mix(in srgb, var(--phosphor) 55%, transparent);
  --scanline-alpha: 0.34;
  --glow-strength: 20px;

  /* Kill the gloss: a phosphor tube has no moulded plastic on it. */
  --gloss-face: linear-gradient(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.4));
  --gloss-spec: none;

  color: var(--phosphor);
  text-shadow: 0 0 8px var(--phosphor);
}

/* Heavier vignette and a hard flicker on the glass. The sweep is not
   named here any more: it lives on body::after now, where it is moved
   by a transform, and it keeps running on its own. */
[data-theme="dark"] body.crt-mode::before {
  --vignette: radial-gradient(ellipse at center, transparent 28%, rgba(0, 0, 0, 0.92) 100%);
  animation: flicker 110ms steps(2, end) infinite;
}

/* Chromatic aberration: the red and blue guns very slightly out of
   convergence, which is what makes a real tube look like a real tube. */
[data-theme="dark"] body.crt-mode .window__title,
[data-theme="dark"] body.crt-mode .stat__value,
[data-theme="dark"] body.crt-mode .client-card__name,
[data-theme="dark"] body.crt-mode h1,
[data-theme="dark"] body.crt-mode h2,
[data-theme="dark"] body.crt-mode h3 {
  text-shadow:
    0 0 10px var(--phosphor),
    -1px 0 rgba(255, 0, 90, 0.55),
     1px 0 rgba(0, 160, 255, 0.55);
}

@keyframes flicker {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.86; }
}

/* ============================================================
   15b. RONIN — the assistant
   Bottom-LEFT on purpose: the toast stack owns the bottom-right corner, and
   two fixed elements in the same corner would sit on top of each other.

   Two states, driven by data-open on the root:
     "false"  collapsed — just the character and his badge
     "true"   expanded  — the panel is open above him
     "aside"  a one-line reaction that closes itself

   He is collapsed by default and only takes space when asked. That is what
   lets him live on every page: an always-open panel in a corner would cover
   a client card here, the findings terminal there.
   ============================================================ */
.ronin {
  position: fixed;
  left: var(--sp-4);
  bottom: var(--sp-4);
  /* Above the page, below modals (1000) and toasts (2000), so he can never
     cover something the user is being asked to read or act on. */
  z-index: 900;
  display: grid;
  gap: var(--sp-2);
  justify-items: start;
  /* The root ignores the mouse so the page underneath stays clickable; only
     his button and his panel take pointer events back. Without this, an
     invisible box in the corner would swallow clicks meant for the content. */
  pointer-events: none;
}

/* ---- The character ---------------------------------------- */

.ronin__btn {
  position: relative;
  padding: 0;
  background: none;
  border: 0;
  line-height: 0;
  pointer-events: auto;
  cursor: pointer;
  transition: transform var(--t-fast) ease-out;
}

.ronin__btn:hover { transform: translateY(-4px) scale(1.05); }
.ronin__btn:active { transform: translateY(0) scale(0.98); }

.ronin__btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 6px;
  border-radius: var(--r-2);
}

/* drop-shadow follows the sprite's alpha channel, so the glow hugs his
   silhouette instead of boxing the whole canvas — which is exactly what
   box-shadow would have done here. */
.ronin__canvas {
  display: block;
  image-rendering: pixelated;
  filter: drop-shadow(0 0 14px color-mix(in srgb, var(--accent) 55%, transparent))
          drop-shadow(0 6px 10px rgba(0, 0, 0, 0.6));
}

/* ---- The badge -------------------------------------------- */

/* A live count of things that need attention. Hidden entirely at zero rather
   than showing "0", because a badge that is always there trains people to
   stop seeing it. */
.ronin__badge {
  position: absolute;
  top: 4px;
  right: -2px;
  min-width: 22px;
  height: 22px;
  padding: 0 5px;
  display: grid;
  place-items: center;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
  color: #fff;
  background: var(--danger);
  border: 1px solid color-mix(in srgb, #fff 40%, transparent);
  border-radius: var(--r-pill);
  box-shadow: var(--bloom-sm) var(--danger);
  animation: ronin-badge-pulse var(--t-pulse) ease-in-out infinite;
}

.ronin__badge[hidden] { display: none; }

@keyframes ronin-badge-pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.12); }
}

/* ---- The panel -------------------------------------------- */

.ronin__panel {
  position: relative;
  width: min(300px, calc(100vw - var(--sp-6) * 2));
  padding: var(--sp-4);
  padding-right: var(--sp-5);
  display: grid;
  gap: var(--sp-2);
  font-size: var(--fs-sm);
  line-height: var(--lh-base);
  color: var(--text);
  /* Solid, not glass. Everything else on these pages is translucent, but this
     is the one surface carrying a sentence someone has to read, and rain
     falling behind text is exactly what makes it unreadable. */
  background: var(--surface);
  border: 1px solid var(--phosphor);
  border-radius: var(--r-3);
  box-shadow: var(--bloom-md) color-mix(in srgb, var(--phosphor) 45%, transparent),
              var(--lift);
  pointer-events: auto;
  animation: ronin-panel-in 240ms ease-out;
}

.ronin__panel[hidden] { display: none; }

@keyframes ronin-panel-in {
  from { opacity: 0; transform: translateY(10px) scale(0.96); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

/* The severity of the advice colours the border and the heading, so the
   seriousness of what he is saying is legible before it is read. Never colour
   ALONE though — the wording always says it too. */
.ronin__panel[data-level="FAIL"] {
  border-color: var(--danger);
  box-shadow: var(--bloom-md) color-mix(in srgb, var(--danger) 45%, transparent), var(--lift);
}
.ronin__panel[data-level="WARN"] {
  border-color: var(--warning);
  box-shadow: var(--bloom-md) color-mix(in srgb, var(--warning) 40%, transparent), var(--lift);
}

.ronin__title {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--phosphor);
}

.ronin__panel[data-level="FAIL"] .ronin__title { color: var(--danger); }
.ronin__panel[data-level="WARN"] .ronin__title { color: var(--warning); }

.ronin__body {
  margin: 0;
  min-height: 1.2em;   /* reserve the line so the panel does not jump while typing */
  color: var(--text-dim);
}

/* The block cursor that terminals blink at the end of a line. */
.ronin__body::after {
  content: "\2588";
  margin-left: 2px;
  color: var(--phosphor);
  animation: blink-caret 900ms steps(2, end) infinite;
}

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

.ronin__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-top: var(--sp-1);
}

.ronin__actions button[hidden] { display: none; }

.ronin__dismiss {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  font-size: var(--fs-xs);
  color: var(--text-faint);
  background: none;
  border: 0;
  border-radius: var(--r-2);
  cursor: pointer;
}

.ronin__dismiss:hover { color: var(--text); background: var(--surface-up); }

/* The little tail pointing down at him. */
.ronin__panel::before {
  content: "";
  position: absolute;
  left: 30px;
  bottom: -7px;
  width: 12px;
  height: 12px;
  background: var(--surface);
  border-right: 1px solid var(--phosphor);
  border-bottom: 1px solid var(--phosphor);
  transform: rotate(45deg);
}

.ronin__panel[data-level="FAIL"]::before { border-color: var(--danger); }
.ronin__panel[data-level="WARN"]::before { border-color: var(--warning); }

/* ============================================================
   16. RESPONSIVE
   ============================================================ */
@media (max-width: 720px) {
  .taskbar {
    flex-wrap: wrap;
    padding: var(--sp-2);
  }

  .taskbar__logo {
    margin-right: 0;
    font-size: var(--fs-display-sm);
  }

  .nav-link {
    padding: var(--sp-2);
    font-size: 8px;
  }

  /* RONIN stands down on small screens. He is 96px wide and fixed to the
     bottom-left, which on a phone means sitting directly on top of the
     client list rather than beside it. */
  .ronin { display: none; }

  .client-card {
    grid-template-columns: 40px 1fr;
    grid-template-areas:
      "avatar body"
      "actions actions";
  }

  .client-card__actions {
    grid-area: actions;
    justify-content: space-between;
    padding-top: var(--sp-3);
    border-top: 2px solid var(--border);
  }

  .avatar { width: 40px; height: 40px; }

  .toast-stack {
    left: var(--sp-2);
    right: var(--sp-2);
    justify-items: stretch;
  }

  .toast { min-width: 0; max-width: none; }
}

/* ============================================================
   19. THE LIGHT THEME'S OWN ACCENTS

   Everything above this line is shared: both themes use the same
   components, the same markup and the same measurements, and differ
   only in the tokens those components read.

   This block is the exception — the handful of places where "Soft
   Club, Haunted" is not a recolouring of "Cyber Chrome" but a
   different object. There are two kinds of rule here:

     1. Switching OFF things that only make sense with light sources
        behind them. Every glow in the app resolves to a grey smear on
        a pale background, and a smear behind body text is a contrast
        bug wearing a costume.

     2. A small set of marks the dark theme never shows at all: the
        stamped badge, the scratched divider, the damp corner. One
        motif each, repeated, so they read as a system rather than as
        noise.

   SELECTOR RULE FOR THIS WHOLE BLOCK: [data-theme="light"] as a
   DESCENDANT, never html[data-theme="light"]. The design-system
   previews put the attribute on a <section> so both themes can be
   shown side by side on one page, and an html-anchored selector
   silently does nothing there.

   The grit stays where it was agreed: texture and display type only.
   No input, client name, money figure, date, error message or line of
   body copy is touched by anything below.
   ============================================================ */

/* ---- 1. Switch off the glow ---------------------------------------
   The bloom tokens are already zeroed in tokens.css, which handles
   every box-shadow. These are the text-shadows, which are written as
   literal blur radii at their call sites and so cannot be reached by
   a token. The terminal is deliberately exempt: it stays a dark CRT
   in a lit room, and a phosphor tube really does glow. */
[data-theme="light"] h1,
[data-theme="light"] h2,
[data-theme="light"] h3,
[data-theme="light"] .auth__brand,
[data-theme="light"] .auth__boot,
[data-theme="light"] .clock {
  text-shadow: none;
}

/* ---- 2. The title bar stops shimmering ----------------------------
   In dark this is a CD surface: an iridescent ramp panning slowly
   under a fixed specular glare. In light the same token is a wet
   slate ramp, and printed slate does not move. Holding it still is
   also what lets the bar read as the header of a document rather
   than as a light fitting. */
[data-theme="light"] .window__bar,
[data-theme="light"] .taskbar::after {
  animation: none;
}

/* ---- 3. Badges become rubber stamps -------------------------------
   Dark's badge is a pixel dot that breathes. Light's is something
   pressed onto the page by hand: squared off, letter-spaced wide,
   knocked very slightly out of true, and inked unevenly.

   The rotation is the whole trick, and it is deliberately tiny. At
   0.8deg it reads as a stamp applied by a person; at 3deg it reads
   as a CSS mistake. */
[data-theme="light"] .badge {
  padding: var(--sp-1) var(--sp-3);
  border-width: 2px;
  border-radius: var(--r-1);
  letter-spacing: 0.14em;
  transform: rotate(-0.8deg);
  /* Ink sits heavier at the edges of a stamp than in the middle,
     because that is where the rubber bites. */
  background-image:
    radial-gradient(ellipse at 50% 50%,
      transparent 30%,
      color-mix(in srgb, currentColor 16%, transparent) 100%);
  background-color: color-mix(in srgb, currentColor 7%, transparent);
  /* An offset second impression, as though the stamp rocked. */
  box-shadow: 1px 1px 0 color-mix(in srgb, currentColor 22%, transparent);
}

/* The pixel dot has no business on a stamp — a stamp is one solid
   impression, not a component with an indicator light. */
[data-theme="light"] .badge::before {
  display: none;
}

/* ---- 4. Scratched dividers ----------------------------------------
   A 1px solid border is the most machine-made mark available. These
   are the same lines, drawn instead as a repeating dash of uneven
   lengths so the rule looks scored into the page rather than printed
   on it. Borders become transparent and the mark moves to a gradient,
   because a gradient can be irregular and a border cannot. */
[data-theme="light"] .recent-row,
[data-theme="light"] .finding__row,
[data-theme="light"] .shortcut-row {
  border-bottom-color: transparent;
  background-image: linear-gradient(
    90deg,
    var(--border) 0 7px, transparent 7px 9px,
    var(--border) 9px 30px, transparent 30px 31px,
    var(--border) 31px 44px, transparent 44px 48px,
    var(--border) 48px 96px, transparent 96px 97px
  );
  background-size: 97px 1px;
  background-repeat: repeat-x;
  background-position: 0 100%;
}

[data-theme="light"] .recent-row:last-of-type,
[data-theme="light"] .finding__row:last-child,
[data-theme="light"] .shortcut-row:last-child {
  background-image: none;
}

/* ---- 5. The damp corner -------------------------------------------
   One found mark, used once per window, top right. A stain where
   something has soaked through from the other side of the page.

   This is the only purely decorative addition in the block, and it is
   kept to a single motif on purpose: the research on modern grunge is
   blunt that one repeated mark reads as a system and three read as a
   mess. It is also pointer-events: none and sits under the content,
   so it can never interfere with anything. .window is already
   position: relative, so this has something to anchor to. */
[data-theme="light"] .window::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  width: 140px;
  height: 120px;
  pointer-events: none;
  z-index: 0;
  border-radius: 0 var(--r-4) 0 0;
  background-image:
    radial-gradient(ellipse 70% 55% at 88% 8%,
      rgba(122, 112, 88, 0.16) 0%,
      rgba(122, 112, 88, 0.07) 45%,
      transparent 72%),
    radial-gradient(ellipse 40% 30% at 74% 26%,
      rgba(104, 110, 96, 0.10) 0%,
      transparent 70%);
}

/* The stain must never come between the user and the interface. */
[data-theme="light"] .window__bar,
[data-theme="light"] .window__body {
  position: relative;
  z-index: 1;
}

/* ---- 6. RONIN is printed, not lit ---------------------------------
   His sprite carries a cyan bloom in dark, which is right: he is a
   neon figure on a black screen. In light the same glow is a grey
   halo around a pixel character. He gets a hard offset ink shadow
   instead — the shadow of something sitting ON the page. */
[data-theme="light"] .ronin__canvas {
  filter: drop-shadow(2px 3px 0 rgba(70, 78, 68, 0.34));
}

/* His panel is the one surface in the light theme that keeps a border
   of pure colour, because it is the one that has to be found quickly. */
[data-theme="light"] .ronin__panel {
  box-shadow: var(--lift);
}

/* ---- 7. BREACH — the light theme's easter egg ---------------------
   The counterpart to the dark theme's CRT mode, triggered by the same
   Konami code (see app.js). The JS half brightens the canvas and floods
   it with figures; this is the room's half of it — the daylight drains
   out of the corners and the walls breathe.

   Built exactly like body.crt-mode::before above it: the vignette layer
   is the one fixed sheet already sitting over the whole interface, so
   overriding it here darkens everything including modals, and it is the
   only thing touched — the scanlines on the same element and the sweep on
   body::after are both left alone.

   Scoped to the light theme through the html attribute, because breach is
   a light-theme effect and its palette is the bruised red of one. The
   selector is a descendant, matching the rest of section 19. */
[data-theme="light"] body.breach-mode::before {
  --vignette: radial-gradient(ellipse at center,
    transparent 16%, rgba(46, 12, 18, 0.66) 100%);
  animation: breach-breathe 3.6s ease-in-out infinite;
}

/* The corners pull in and ease back out — a room closing around you. */
@keyframes breach-breathe {
  0%, 100% { transform: scale(1); opacity: 0.88; }
  50%      { transform: scale(1.04); opacity: 1; }
}

/* ============================================================
   20. VITALS — the heartbeat readout

   A small instrument well beside a client's status, showing a trace
   that says how alive the deal is:

     Lead       a single sparse blip, slow — barely monitored yet
     Contacted  a faint but regular rhythm
     Won        a strong healthy QRS complex
     Lost       flatline, and it does not move

   BOTH THEMES. This is the one addition in this branch that is not a
   light-theme thing: it is built entirely out of existing tokens, so
   dark gets a neon readout and light gets an ink one from exactly the
   same rules.

   HOW IT IS DRAWN
   The trace is an SVG line used as a MASK, not as an image, with the
   colour supplied underneath by `background-color: currentColor`. That
   indirection is the whole trick — an <img> would have its colour baked
   in and would need four files per theme, whereas a mask takes its
   colour from whatever the element inherits. The badge classes already
   set that colour per status, so the readout matches its badge without
   being told the status twice.

   HOW IT MOVES
   The trace element is twice the width of its window and the mask tiles
   at half that, so exactly two periods sit side by side. Sliding it
   left by 50% therefore lands the second period precisely where the
   first was, and the loop is seamless with no visible jump. The slide
   is a `transform`, so it runs on the compositor — which matters,
   because a list of thirty clients means thirty of these running at
   once and anything that repainted would be thirty repaints a frame.
   ============================================================ */

.status-row {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

.vitals {
  position: relative;
  display: inline-block;
  flex-shrink: 0;
  width: 52px;
  height: 20px;
  overflow: hidden;
  vertical-align: middle;

  /* A recessed instrument well, the same one the dashboard clock sits in. */
  background: var(--bg-deep);
  border: 1px solid var(--border);
  border-radius: var(--r-1);
  box-shadow: var(--bevel-in);
}

.vitals__trace {
  position: absolute;
  inset: 0;
  width: 200%;

  background-color: currentColor;

  -webkit-mask-repeat: repeat-x;
  mask-repeat: repeat-x;
  -webkit-mask-size: 50% 100%;
  mask-size: 50% 100%;
  -webkit-mask-position: 0 50%;
  mask-position: 0 50%;

  animation: vitals-scroll 2.6s linear infinite;
}

@keyframes vitals-scroll {
  to { transform: translateX(-50%); }
}

/* ---- Lead: one tiny blip, a lot of flat, slow ---------------------
   The quietest of the three that is still alive. A single small
   deflection sits alone in a long flatline and scrolls past slowly, so
   a lead reads as "barely monitored yet" — and reads as plainly
   DIFFERENT from a contacted client, which was the point of the change:
   before, lead and contacted were both faint and looked alike. */
.vitals--lead { color: var(--status-lead); }

.vitals--lead .vitals__trace {
  animation-duration: 3.8s;
  opacity: 0.8;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 40' preserveAspectRatio='none'%3E%3Cpath d='M0 20 L50 20 L53 17 L56 23 L59 20 L100 20' fill='none' stroke='%23fff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 40' preserveAspectRatio='none'%3E%3Cpath d='M0 20 L50 20 L53 17 L56 23 L59 20 L100 20' fill='none' stroke='%23fff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

/* ---- Contacted: a clear, confident, regular beat ------------------
   Now a proper single spike — a small dip into a tall clean peak — at
   full strength, medium tempo. It is unmistakably a pulse, where the
   lead is a twitch. That contrast is the whole distinction the two
   stages are meant to carry. */
.vitals--contacted { color: var(--status-contacted); }

.vitals--contacted .vitals__trace {
  animation-duration: 2.5s;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 40' preserveAspectRatio='none'%3E%3Cpath d='M0 20 L40 20 L45 20 L48 24 L51 9 L54 31 L57 20 L62 20 L100 20' fill='none' stroke='%23fff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 40' preserveAspectRatio='none'%3E%3Cpath d='M0 20 L40 20 L45 20 L48 24 L51 9 L54 31 L57 20 L62 20 L100 20' fill='none' stroke='%23fff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

/* ---- Won: a strong, healthy complex, fast -------------------------
   The full P-QRS-T of a healthy beat, tallest of the three, and now
   the FASTEST — a won deal has the most vigorous pulse in the list. */
.vitals--won { color: var(--status-won); }

.vitals--won .vitals__trace {
  animation-duration: 1.3s;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 40' preserveAspectRatio='none'%3E%3Cpath d='M0 20 L18 20 L22 17 L26 20 L38 20 L42 6 L46 34 L50 20 L62 20 L66 16 L70 20 L100 20' fill='none' stroke='%23fff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 40' preserveAspectRatio='none'%3E%3Cpath d='M0 20 L18 20 L22 17 L26 20 L38 20 L42 6 L46 34 L50 20 L62 20 L66 16 L70 20 L100 20' fill='none' stroke='%23fff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

/* ---- Lost: flatline ------------------------------------------------
   The animation is removed rather than slowed. A flatline that scrolls
   is still moving, and movement is the one thing this state must not
   have. */
.vitals--lost { color: var(--status-lost); }

.vitals--lost .vitals__trace {
  animation: none;
  transform: none;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 40' preserveAspectRatio='none'%3E%3Cpath d='M0 20 L100 20' fill='none' stroke='%23fff' stroke-width='2.5' stroke-linecap='round'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 40' preserveAspectRatio='none'%3E%3Cpath d='M0 20 L100 20' fill='none' stroke='%23fff' stroke-width='2.5' stroke-linecap='round'/%3E%3C/svg%3E");
}

/* ---- The panel variant: a monitor running behind a readout ---------
   Same component, stretched across the width of an analytics panel and
   dropped to a whisper. It is not reporting on anything — it is the
   sound of the room the diagnostic is being run in. */
.vitals--panel {
  display: block;
  width: 100%;
  height: 30px;
  margin-top: var(--sp-3);
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  color: var(--phosphor);
  opacity: 0.3;
}

.vitals--panel .vitals__trace {
  -webkit-mask-size: 25% 100%;
  mask-size: 25% 100%;
  animation-duration: 4.2s;
}

/* Reduced motion: the trace stays, the scrolling stops. The SHAPE is the
   information — a flatline still reads as a flatline standing still, and
   the status is spelled out in words on the badge beside it regardless. */
@media (prefers-reduced-motion: reduce) {
  .vitals__trace { animation: none; }
}
