/* Fun Hub — the hub's own skin. Dark first, purple for interactive, coral for the single
   primary action per view, green for good.
   This is a dense working tool, and round 4 (qa/reviews/round-4.md) says it is allowed to have a
   face: the data stays tight, and everything around it — the head, the rail, the marks, the states
   where there is no data — is drawn, is bigger, and has a voice.
   Two faces: Plus Jakarta Sans reads, Bricolage Grotesque speaks (titles, figures, the wordmark). */

:root {
  color-scheme: dark;

  /* brand */
  --purple: #6a5fd3;
  --purple-light: #8a7fe0;
  --purple-dark: #4e45a8;
  --purple-glow: rgba(106, 95, 211, 0.18);

  /* ── Coral vs status red — settled in qa/reviews/round-2.md §0.1 ──────────────
     The brand coral (#ed5b3e) and the status red (--crit #f2555a) are ΔE 3.7 apart
     in OKLab: below the ΔE ≥ 8 a reader needs to tell two colours apart, and the
     reason the two kept being confused. White on #ed5b3e also measures 3.41:1,
     under the 4.5:1 this project promises.

     So the brand coral stays as the *decoration* value (glows, non-text washes),
     and everything that carries a label uses --coral-strong:
        white on --coral-strong  4.87:1  ✓
        --coral-strong vs --crit ΔE 10.2 ✓

     The rule these tokens exist to enforce:
       · Coral is a FILLED SURFACE and nothing else — one per view, a pill with a
         white label. Never text, never an icon, never a lone border, never a mark.
       · Status red is INK, a hairline, or a 14 % tint — never a filled action.
     Break either half and the two become confusable again whatever the hex says. */
  --coral: #ed5b3e;
  --coral-strong: #c94327;
  --coral-press: #b8391f;
  --coral-glow: rgba(237, 91, 62, 0.28);

  /* surfaces */
  --bg: #0c0c10;
  --bg-rail: #0a0a0e;
  --bg-2: #111118;
  --surface: #18181f;
  --surface-2: #1e1e27;
  --line: rgba(255, 255, 255, 0.08);
  --line-2: rgba(255, 255, 255, 0.14);
  --line-3: rgba(255, 255, 255, 0.24);

  /* ink — every value used for text clears 4.5:1 on --surface */
  --ink: #ffffff;
  --ink-2: rgba(255, 255, 255, 0.68);
  --ink-3: rgba(255, 255, 255, 0.54);
  --ink-4: rgba(255, 255, 255, 0.38); /* decoration only, never text */

  /* status — always paired with an icon and a word */
  --ok: #1ea877;
  --ok-soft: rgba(30, 168, 119, 0.14);
  --warn: #f0ad3c;
  --warn-soft: rgba(240, 173, 60, 0.13);
  --crit: #f2555a;
  --crit-soft: rgba(242, 85, 90, 0.14);
  --info: #8aa0cc;
  --info-soft: rgba(138, 160, 204, 0.14);

  --radius-sm: 8px;
  --radius: 10px;
  --radius-lg: 16px;
  --radius-xl: 24px;

  /* One 4px spacing scale. Everything that used to be 13/15/18/26 rounds onto it. */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 20px;
  --s-6: 24px;
  --s-7: 32px;
  --s-8: 48px;

  /* Motion. One duration band, one easing, everything off under prefers-reduced-motion. */
  --ease: cubic-bezier(0.22, 0.7, 0.3, 1);
  --t-fast: 120ms;
  --t: 180ms;
  --t-slow: 240ms;

  /* Prose measure. `ch` is the width of a `0`, which is far wider than the average letter, so
     `62ch` was rendering ~90 characters a line. `em` lands where the eye wants it: ~66. */
  --measure: 30em;
  --measure-wide: 36em;

  --rail-w: 232px;
  --page-max: 1460px;
  --font: 'Plus Jakarta Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
  /* The display voice. Falls back to the reading face, so a blocked font request costs character
     and nothing else — no metric surprise, no reflow past what `swap` already does. */
  --font-display: 'Bricolage Grotesque', 'Plus Jakarta Sans', system-ui, sans-serif;
  --mono: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace;

  /* ── One type scale. Round 4 §1 counted 28 rules setting text below 13px and nine below 11px;
     these eight values replace all of them. --fs-xs is the FLOOR: nothing in this app is smaller,
     and it is reserved for uppercase micro-labels, where the caps make up the difference. */
  --fs-xs: 12px;   /* uppercase labels, table headers, captions */
  --fs-sm: 13px;   /* secondary and meta text */
  --fs-base: 15px; /* body, forms, buttons, nav */
  --fs-md: 16px;   /* a sentence that matters — a flag, a dialog */
  --fs-lg: 19px;   /* section headings */
  --fs-xl: 22px;   /* card titles, the verdict */
  --fs-2xl: 27px;  /* secondary figures */
  --fs-3xl: 34px;  /* page titles, stat figures */

  /* Elevation. Round 2 gave every component the same flat surface and hierarchy had nowhere to
     live but font size; these two let a card be a card. */
  --lift: 0 1px 0 rgba(255, 255, 255, 0.04) inset, 0 6px 20px -14px rgba(0, 0, 0, 0.9);
  --lift-hover: 0 1px 0 rgba(255, 255, 255, 0.07) inset, 0 14px 34px -18px rgba(0, 0, 0, 0.95),
                0 0 0 1px rgba(106, 95, 211, 0.18);

  /* chart tokens — the shared chart kit reads only these */
  --viz-surface: var(--surface);
  --viz-surface-ring: var(--surface);
  --viz-ink: var(--ink);
  --viz-ink-2: var(--ink-2);
  --viz-ink-3: var(--ink-3);
  --viz-ink-4: rgba(255, 255, 255, 0.52);
  --viz-grid: rgba(255, 255, 255, 0.07);
  --viz-axis: rgba(255, 255, 255, 0.26);
  --viz-border: var(--line);
  --viz-border-strong: var(--line-2);
  --viz-track: rgba(255, 255, 255, 0.08);
  --viz-focus: var(--purple-light);
  --viz-tip-bg: #22222c;
  --viz-tip-border: rgba(255, 255, 255, 0.14);
  --viz-tip-ink: var(--ink);
  --viz-tip-ink-2: var(--ink-3);
  /* Validated against the #18181f card surface with the dataviz six-checks script.
     Only slot 1 is in use today; the rest are here so the panel inherits a safe order. */
  --viz-series-1: #3987e5;
  --viz-series-2: #d95926;
  --viz-series-3: #199e70;
  --viz-series-4: #c98500;
  --viz-series-5: #d55181;
  --viz-series-6: #008300;
  --viz-series-7: #9085e9;
  --viz-series-8: #e66767;
  --viz-muted: #7c7c8a;
  /* Twelve of these run down the sites table. At 0.36 they were the loudest thing in a quiet table
     and read as decoration; the number beside them is the datum, the spark is only its shape. */
  --viz-spark: rgba(255, 255, 255, 0.22);
  --viz-good: var(--ok);
  --viz-warn: var(--warn);
  --viz-crit: var(--crit);
  --viz-none: rgba(255, 255, 255, 0.11);
}

/* ------------------------------------------------------------------ base */

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

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--fs-base);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Everything the display face is allowed to touch. It is never used where one number is compared
   with another: Bricolage's figures are proportional and a column of percentages set in it reads
   drunk (round 4 §7). */
.page-title, .block-title, .stat-value, .brand-name, .login-title, .login-wordmark,
.dialog-title, .hero-text, .chart-total, .report-client, .card-title, .client-card-name,
.boot-title, .all-clear-title, .not-found-title {
  font-family: var(--font-display);
  font-optical-sizing: auto;
}

h1, h2, h3, h4, p, dl, dd, figure, ol, ul { margin: 0; }
ol, ul { padding: 0; list-style: none; }

a { color: var(--purple-light); text-decoration: none; }
a:hover { color: #a79ef0; }

button, input, select, textarea { font: inherit; color: inherit; }

/* White, not purple. The ring used to be `--purple-light`, which measured 1.43:1 on the coral
   primary button, 1.48:1 on the language pills and 1.74:1 on the mock chip — the three controls it
   was most likely to land on. WCAG 1.4.11 wants 3:1 against whatever is behind it, and on a
   dark-only app white clears that everywhere with room to spare. */
:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
  border-radius: 4px;
}

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: fixed;
  top: 8px; left: 8px;
  z-index: 100;
  padding: 8px 14px;
  background: var(--purple);
  color: #fff;
  border-radius: 999px;
  font-weight: 600;
  transform: translateY(-160%);
  transition: transform 140ms ease;
}
.skip-link:focus { transform: translateY(0); color: #fff; }

/* WCAG 2.2 AA (2.5.8) wants 24px in both directions for anything that is not a link inside a
   sentence. These were 19–23px high on a phone; the padding replaces margin that was already
   there, so nothing moved. */
.back-link, .brand, .plain-link, .host-link, .subtle-link {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
}

/* Only the stacked-card form prints a column name beside each value; the table has a header row. */
.cell-label { display: none; }

.muted { color: var(--ink-3); }
.num { font-variant-numeric: tabular-nums; }

::selection { background: rgba(106, 95, 211, 0.45); color: #fff; }

/* A dark page with a bright system scrollbar down the side is the one thing that gives away
   "a web app in a light OS". Firefox takes the two-colour form; Chromium takes the parts. */
/* A flick that runs out of table chains to the page and then to the browser's pull-to-refresh,
   which reloads the whole single-page app and loses where you were. */
.table-wrap, .qs-list, .viz-table, .plain-text-body, .mail-stage { overscroll-behavior: contain; }

* { scrollbar-color: rgba(255, 255, 255, 0.18) transparent; scrollbar-width: thin; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.14);
  border: 2px solid transparent;
  background-clip: content-box;
  border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.26); background-clip: content-box; }
::-webkit-scrollbar-corner { background: transparent; }

/* ------------------------------------------------------------- motion -- */
/* A few purposeful transitions, one easing, one band. Everything here is switched off by the
   prefers-reduced-motion block at the foot of this file. */

.outlet > .page { animation: page-in var(--t) var(--ease) both; }
@keyframes page-in { from { opacity: 0; transform: translateY(6px); } }

.dialog[open] { animation: dialog-in var(--t) var(--ease) both; }
.dialog[open]::backdrop { animation: fade-in var(--t) var(--ease) both; }
@keyframes dialog-in { from { opacity: 0; transform: translateY(8px) scale(0.985); } }
@keyframes fade-in { from { opacity: 0; } }

/* ----------------------------------------------------------------- shell */

.shell { display: flex; min-height: 100vh; }

.rail {
  position: sticky;
  top: 0;
  flex: 0 0 var(--rail-w);
  width: var(--rail-w);
  height: 100vh;
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
  padding: var(--s-5) var(--s-3) var(--s-4);
  background: var(--bg-rail);
  border-right: 1px solid var(--line);
  /* The one atmospheric thing in the app: a purple bloom behind the mark, so the rail has a top
     rather than just ending. It is 6 % at its strongest and gone by a third of the way down. */
  background-image: radial-gradient(120% 34% at 0% 0%, rgba(106, 95, 211, 0.16), transparent 70%);
}

.rail-top { display: flex; flex-direction: column; gap: var(--s-3); padding: 0 var(--s-2); }

.brand { display: inline-flex; align-items: center; gap: 10px; color: var(--ink); }
.brand:hover { color: var(--ink); }

/* The Fun Hub mark — a sibling of the real Fun Systems mark rather than a rival to it. Same
   isometric cube, same three faces in the same three brand colours, same geometry as
   `brand-assets/funsystems/mark.svg`; the one addition is the uptime pulse on the left face,
   because this is the tool that watches. One mark everywhere: rail, favicon, app icon, login.
   The SVG is a data: URL in a background-image, which the CSP's `img-src 'self' data:` allows,
   and it is the same drawing as `public/shared/icons/hub.svg` — change both together.

   Coral appears here as a face fill. That does not break round 2 §0.1: the rule governs the UI's
   own action and status surfaces, and a logo is a logo. It is never interactive, never a status,
   and never carries a label. */
.brand-mark {
  width: 36px; height: 36px;
  flex: none;
  border-radius: 11px;
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpolygon points='50,12 82,30 50,48 18,30' fill='%23ed5b3e'/%3E%3Cpolygon points='18,30 50,48 50,86 18,68' fill='%236a5fd3'/%3E%3Cpolygon points='82,30 50,48 50,86 82,68' fill='%231ea877'/%3E%3Cpath d='M21.2 52.7 27.6 56.3 32.7 46.6 37.8 73.5 46.8 67.1' fill='none' stroke='%23fff' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 104% no-repeat,
    #0d0d1a;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.12) inset, 0 6px 20px -8px var(--purple-glow);
  transition: transform var(--t) var(--ease), box-shadow var(--t) var(--ease);
}
.brand:hover .brand-mark {
  transform: rotate(-4deg) scale(1.06);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.22) inset, 0 10px 26px -8px rgba(106, 95, 211, 0.6);
}
.brand-mark--lg { width: 52px; height: 52px; border-radius: 15px; }

/* The real Fun Systems wordmark, quietly, at the foot of the rail — the one place in the hub where
   the studio signs its own tool. It is the file from `brand-assets/`, unmodified and unrecoloured;
   `logo-on-dark.svg` is the variant drawn for exactly this background. */
/* The rail used to carry three drifting cubes in the space under the links. They came out: the
   cube is one object now, it lives in the hero, and a scatter of it in the rail read as random
   (Marcelo's word). The empty space pushes the search box and the signature to the foot, which is
   where they belong anyway (`.rail-foot` takes the `margin-top: auto` the motif used to hold). */

.rail-credit { display: flex; padding: var(--s-3) var(--s-2) 0; border-top: 1px solid var(--line); }
.rail-credit img { width: 104px; height: auto; opacity: 0.4; transition: opacity var(--t) ease; }
.rail-credit:hover img { opacity: 0.7; }

.brand-name { font-size: var(--fs-xl); font-weight: 800; letter-spacing: -0.028em; }

.mock-chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  align-self: flex-start;
  padding: 4px 11px 4px 9px;
  border-radius: 999px;
  background: var(--warn-soft);
  border: 1px solid rgba(240, 173, 60, 0.32);
  color: var(--warn);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
}
.mock-chip:hover { color: var(--warn); border-color: rgba(240, 173, 60, 0.6); }

.mock-dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: currentColor;
  animation: pulse 2.4s ease-in-out infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

.nav { display: flex; flex-direction: column; gap: 3px; }

.nav-link {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 9px 11px;
  border-radius: var(--radius);
  color: var(--ink-2);
  font-size: var(--fs-base);
  font-weight: 600;
  letter-spacing: -0.005em;
  transition: background var(--t-fast) ease, color var(--t-fast) ease;
}
.nav-link:hover { background: rgba(255, 255, 255, 0.05); color: var(--ink); }
.nav-link .icon { color: var(--ink-3); flex: none; transition: transform var(--t) var(--ease); }
.nav-link:hover .icon { color: var(--ink-2); transform: translateX(1px); }

.nav-link.is-active {
  background: linear-gradient(90deg, rgba(106, 95, 211, 0.26), rgba(106, 95, 211, 0.05));
  color: var(--ink);
  box-shadow: inset 3px 0 0 var(--purple-light);
}
.nav-link.is-active .icon { color: var(--purple-light); }

.rail-foot { margin-top: auto; display: flex; flex-direction: column; gap: var(--s-3); padding: 0 var(--s-1); }

.quick-search {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 9px 11px;
  background: rgba(255, 255, 255, 0.035);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--ink-3);
  font-size: var(--fs-base);
  cursor: pointer;
  text-align: left;
}
.quick-search:hover { border-color: var(--line-2); color: var(--ink-2); background: rgba(255, 255, 255, 0.06); }
.quick-search span { flex: 1; }
.quick-search kbd {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  padding: 2px 7px;
  border: 1px solid var(--line-2);
  border-radius: 5px;
  color: var(--ink-3);
}

.lang { display: inline-flex; padding: 3px; gap: 2px; background: rgba(255, 255, 255, 0.04); border-radius: 999px; align-self: flex-start; }
.lang-btn {
  border: 0;
  background: transparent;
  color: var(--ink-3);
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: 0.05em;
  padding: 6px 14px;
  border-radius: 999px;
  cursor: pointer;
}
.lang-btn[aria-pressed='true'] { background: var(--purple); color: #fff; }
.lang-btn:hover[aria-pressed='false'] { color: var(--ink); }

.pane { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.topbar { display: none; }
.tabbar { display: none; }

.main { flex: 1; min-width: 0; }

.outlet { padding: var(--s-7) var(--s-7) var(--s-8); max-width: var(--page-max); }

/* ------------------------------------------------------------ page head */

.page { display: flex; flex-direction: column; gap: var(--s-6); }

/* `position: relative` + a z-index so the head paints over the hero: at the top of a jump the cube
   is level with the "Add site" button, and passing behind it reads as depth where passing over it
   read as a mistake. */
.page-head { position: relative; z-index: 1; display: flex; flex-direction: column; gap: var(--s-3); }

.back-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-3);
  align-self: flex-start;
}
.back-link:hover { color: var(--ink-2); }
.back-link .flip { transform: rotate(180deg); }

.page-head-row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
}

.page-head-main { min-width: 0; }

.page-title {
  font-size: var(--fs-3xl);
  font-weight: 800;
  letter-spacing: -0.028em;
  line-height: 1.08;
}
.page-title:focus { outline: none; }

.page-sub { margin-top: 6px; font-size: var(--fs-base); color: var(--ink-2); }
.page-meta { margin-top: 5px; font-size: var(--fs-sm); color: var(--ink-3); }

.page-actions, .page-actions-slot { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }

/* ---------------------------------------------------------------- hero --
   The overview's answer to its own question, and the one moment in the hub that is allowed to be
   a *moment*. Round 4's first pass made it a flat band with a 72px grey radar in it and the honest
   verdict on that was "still a generic dark dashboard": this is the second pass.

   It is still the only place where a drawing sits beside live copy, and it still earns it, because
   there is no data here — only the conclusion drawn from it. Everything the picture says is also in
   the sentence beside it, and the picture is `aria-hidden`.

   Round 4c cut it from 176px to 128px, because the sites table now has to start above the fold —
   and `overflow` went from hidden to visible so the cube can jump out of the band. The band still
   clips nothing it needs to: the backgrounds are painted inside the border box and its radius, and
   the top hairline fades to transparent before it reaches a corner. */

.hero {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
  margin-top: var(--s-2);
  padding: var(--s-4) var(--s-6);
  min-height: 128px;
  border-radius: var(--radius-xl);
  border: 1px solid var(--line-2);
  overflow: visible;
  /* Three layers: a wide purple bloom from the left, a tone-specific one from the right, and the
     card surface under both. 12 % at the strongest point, which is the corner, not the text. */
  background:
    radial-gradient(120% 180% at 100% 30%, var(--hero-tint, rgba(106, 95, 211, 0.16)), transparent 58%),
    radial-gradient(90% 160% at 0% 100%, rgba(106, 95, 211, 0.12), transparent 62%),
    linear-gradient(180deg, #1b1b24, #141419);
  box-shadow: var(--lift);
}
/* A hairline of the tone along the top edge, so the hero has a lid. */
.hero::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--hero-edge, rgba(138, 127, 224, 0.6)) 30%, var(--hero-edge, rgba(138, 127, 224, 0.6)) 70%, transparent);
}
.hero--critical { --hero-tint: rgba(242, 85, 90, 0.16); --hero-edge: rgba(242, 85, 90, 0.55); }
.hero--warning { --hero-tint: rgba(240, 173, 60, 0.15); --hero-edge: rgba(240, 173, 60, 0.5); }
.hero--clear { --hero-tint: rgba(30, 168, 119, 0.16); --hero-edge: rgba(30, 168, 119, 0.5); }

/* Wide enough that the verdict — always a short sentence — gets one line at 1440, which is
   worth ~34px of the fold. It still wraps long before it becomes a line of prose. */
.hero-body { min-width: 0; max-width: 46em; display: flex; flex-direction: column; gap: 6px; }
/* Display size, because this is the sentence the screen exists to say. */
.hero-text {
  font-family: var(--font-display);
  font-optical-sizing: auto;
  font-size: clamp(21px, 2vw, 28px);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.1;
  color: var(--ink);
  text-wrap: balance;
}
.hero-sub { font-size: var(--fs-base); color: var(--ink-2); }
/* The cube's box is taller than the band it sits in — it has to be, it jumps. So the slot is the
   band's own height and the cube is centred inside it and allowed out, which keeps the hero sized
   by its sentence (no layout shift, ever) and lets the jump leave the band.

   The stage takes no pointer events *here*: at the top of a jump it is over the page header, and a
   decoration must never eat a click meant for "Add site". The slot catches the tap instead, and it
   is exactly as tall as the band. */
.hero-art-slot { position: relative; align-self: stretch; flex: none; width: var(--cube-size, 128px); cursor: pointer; }
.hero .cube-stage { position: absolute; left: 0; top: 50%; transform: translateY(-50%); pointer-events: none; }

/* The two links under the sentence: the way down to the list, and the mock-data note that used to
   be an 80px banner of its own under the hero. */
.hero-links { display: flex; align-items: center; gap: var(--s-4); flex-wrap: wrap; margin-top: 2px; }
.hero-jump {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: var(--fs-base);
  font-weight: 700;
  color: var(--purple-light);
}
.hero-jump:hover { color: var(--ink); }
.hero-jump .icon { transform: rotate(90deg); }
.hero-mock {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  font-size: var(--fs-sm);
  color: var(--ink-3);
}
.hero-mock-tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--warn);
  font-size: var(--fs-xs);
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.hero-mock:hover { color: var(--ink-2); }
.hero-mock:hover .hero-mock-link { color: var(--ink); }
.hero-mock-link { color: var(--purple-light); font-weight: 600; }

/* ------------------------------------------------------- the brand cube --
   One cube, six faces, real 3D. The maths, once:

     edge  L = W / √2          (W is the width the cube covers on screen)
     the resting pose rotateX(-34.23°) rotateY(45°) projects the top face to a diamond of
     half-width L/√2 and half-height (L/√2)·sin 34.23° = 0.5625 — the 18/32 of the real mark,
     `brand-assets/funsystems/mark.svg`, to within a percent.

   So at rest this is the mark: coral top, purple left, green right, and no motion at all. The
   three faces behind it are the same three colours, darkened, because a tumble shows them.

   Every moving property is a transform or an opacity, `will-change` is set only for the ~1.5s a
   bounce lasts, and the whole thing is inert under `prefers-reduced-motion`. */

.cube-stage {
  /* Any ancestor may set --cube-size; the stage resolves it once, here. */
  --cube-w: var(--cube-size, 128px);
  --cube-edge: calc(var(--cube-w) * 0.7071);
  --cube-jump: -38%;
  --cube-turn: 360deg;
  --cube-dur: 1500ms;
  position: relative;
  flex: none;
  width: var(--cube-w);
  height: calc(var(--cube-w) * 1.34);
  perspective: calc(var(--cube-w) * 7);
  cursor: pointer;
}
.cube-hop, .cube-tilt, .cube-spin, .cube-body { transform-style: preserve-3d; }
.cube-hop {
  position: absolute;
  left: calc((var(--cube-w) - var(--cube-edge)) / 2);
  top: calc(var(--cube-w) * 0.313);
  width: var(--cube-edge);
  height: var(--cube-edge);
}
.cube-tilt, .cube-spin, .cube-body { position: absolute; inset: 0; }
.cube-body { transform: rotateX(-34.23deg) rotateY(45deg); }

.cube-face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.07);
}
/* Light from the top left, so the three faces are not three flat fills. */
.cube-face--top    { transform: rotateX(90deg) translateZ(calc(var(--cube-edge) / 2));  background: linear-gradient(150deg, #ff7f63, #ed5b3e 52%, #d8482c); }
.cube-face--left   { transform: rotateY(-90deg) translateZ(calc(var(--cube-edge) / 2)); background: linear-gradient(165deg, #7f74e4, #6a5fd3 58%, #544ab6); }
.cube-face--front  { transform: translateZ(calc(var(--cube-edge) / 2));                 background: linear-gradient(165deg, #2ac48d, #1ea877 58%, #16855d); }
.cube-face--bottom { transform: rotateX(-90deg) translateZ(calc(var(--cube-edge) / 2)); background: linear-gradient(150deg, #7c2d1b, #572013); }
.cube-face--right  { transform: rotateY(90deg) translateZ(calc(var(--cube-edge) / 2));  background: linear-gradient(165deg, #372f7f, #272062); }
.cube-face--back   { transform: rotateY(180deg) translateZ(calc(var(--cube-edge) / 2)); background: linear-gradient(165deg, #116347, #0b4732); }

/* The reward flash on the green face: all-clear only, at the moment it lands. */
.cube-face--front::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(closest-side, rgba(255, 255, 255, 0.7), transparent);
  opacity: 0;
}

/* The bloom sits low, under the cube rather than behind it: it is the light the cube stands in, and
   it is also what makes the contact shadow readable — black on a #141419 band is invisible, black
   on a purple bloom is a shadow. */
.cube-glow {
  position: absolute;
  left: -24%; top: 16%;
  width: 148%; height: 94%;
  background: radial-gradient(closest-side, var(--cube-bloom, rgba(106, 95, 211, 0.42)), transparent);
  opacity: 0.85;
  pointer-events: none;
}
.cube-stage--clear { --cube-bloom: rgba(30, 168, 119, 0.3); --cube-jump: -54%; --cube-turn: 720deg; --cube-dur: 1750ms; }

/* The contact shadow. It squashes when the cube squashes and shrinks when it leaves the ground —
   without it the jump reads as a slide. */
.cube-shadow {
  position: absolute;
  left: 13%; bottom: calc(var(--cube-w) * 0.02);
  width: 74%; height: calc(var(--cube-w) * 0.15);
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(0, 0, 0, 0.92), rgba(0, 0, 0, 0.45) 58%, transparent);
  opacity: 0.8;
  pointer-events: none;
}

/* The bounce: anticipation, launch, a hang at the top, the tumble, then a landing that settles in
   three decreasing beats. ~1.5s, once. */
@keyframes cube-hop {
  0%   { transform: translate3d(0, 0, 0) scale3d(1, 1, 1); animation-timing-function: cubic-bezier(0.4, 0, 0.55, 1); }
  10%  { transform: translate3d(0, 5%, 0) scale3d(1.1, 0.87, 1); animation-timing-function: cubic-bezier(0.2, 0.5, 0.4, 1); }
  20%  { transform: translate3d(0, -8%, 0) scale3d(0.9, 1.15, 1); animation-timing-function: cubic-bezier(0.14, 0.62, 0.3, 1); }
  46%  { transform: translate3d(0, var(--cube-jump), 0) scale3d(1, 1, 1); animation-timing-function: cubic-bezier(0.62, 0, 0.86, 0.42); }
  70%  { transform: translate3d(0, -4%, 0) scale3d(0.94, 1.1, 1); animation-timing-function: cubic-bezier(0.4, 0, 0.7, 1); }
  77%  { transform: translate3d(0, 4%, 0) scale3d(1.15, 0.83, 1); animation-timing-function: cubic-bezier(0.2, 0.8, 0.4, 1); }
  85%  { transform: translate3d(0, -8%, 0) scale3d(0.97, 1.06, 1); }
  93%  { transform: translate3d(0, 1.5%, 0) scale3d(1.05, 0.95, 1); }
  100% { transform: translate3d(0, 0, 0) scale3d(1, 1, 1); }
}

/* The flip: one axis, a whole number of turns, so the last frame is the resting pose exactly.
   The small negative wind-up at 18 % is what makes it a jump rather than a spin. */
@keyframes cube-spin {
  0%, 8% { transform: rotate3d(1, 0.62, 0.14, 0deg); animation-timing-function: cubic-bezier(0.4, 0, 0.6, 1); }
  18%    { transform: rotate3d(1, 0.62, 0.14, -16deg); animation-timing-function: cubic-bezier(0.15, 0.5, 0.45, 1); }
  48%    { transform: rotate3d(1, 0.62, 0.14, calc(var(--cube-turn) * 0.5)); animation-timing-function: linear; }
  76%    { transform: rotate3d(1, 0.62, 0.14, calc(var(--cube-turn) * 0.94)); animation-timing-function: cubic-bezier(0.25, 0.85, 0.4, 1); }
  87%    { transform: rotate3d(1, 0.62, 0.14, calc(var(--cube-turn) + 10deg)); }
  100%   { transform: rotate3d(1, 0.62, 0.14, var(--cube-turn)); }
}

@keyframes cube-shadow {
  0%   { transform: scale(1); opacity: 0.8; }
  10%  { transform: scale(1.12); opacity: 0.88; }
  46%  { transform: scale(0.5); opacity: 0.26; }
  70%  { transform: scale(0.9); opacity: 0.66; }
  77%  { transform: scale(1.18); opacity: 0.92; }
  85%  { transform: scale(0.84); opacity: 0.6; }
  100% { transform: scale(1); opacity: 0.8; }
}

@keyframes cube-flash { 0%, 68% { opacity: 0; } 79% { opacity: 0.9; } 100% { opacity: 0; } }
@keyframes cube-bloom { 0%, 68% { opacity: 0.75; } 79% { opacity: 1; } 100% { opacity: 0.75; } }

.cube-stage.is-playing .cube-hop,
.cube-stage.is-playing .cube-spin { will-change: transform; }
.cube-stage.is-playing .cube-hop { animation: cube-hop var(--cube-dur) both; }
.cube-stage.is-playing .cube-spin { animation: cube-spin var(--cube-dur) both; }
.cube-stage.is-playing .cube-shadow { animation: cube-shadow var(--cube-dur) both; }
.cube-stage--clear.is-playing .cube-face--front::after { animation: cube-flash var(--cube-dur) both; }
.cube-stage--clear.is-playing .cube-glow { animation: cube-bloom var(--cube-dur) both; }

.page-body { display: flex; flex-direction: column; }

/* A proper identity header: the mark at 56px, the name at page-title size, the state beside it, and
   the host/client/kind line under the pair rather than under the title alone. */
.site-title-row { display: flex; align-items: center; gap: var(--s-4); flex-wrap: wrap; }
.site-title-names { min-width: 0; display: flex; flex-direction: column; gap: 5px; }
.site-title-line { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; }
.site-title-row .page-title { line-height: 1.05; }
.site-identity { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; font-size: var(--fs-base); }
.dot-sep { color: var(--ink-4); }

.host-link { display: inline-flex; align-items: center; gap: 5px; color: var(--ink-2); font-weight: 600; }
.host-link:hover { color: var(--purple-light); }

.site-error {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: var(--s-2);
  color: var(--crit);
  font-size: var(--fs-sm);
}

/* ------------------------------------------------------------- sections */

.stack { display: flex; flex-direction: column; gap: var(--s-7); }
.stack-sm { display: flex; flex-direction: column; gap: var(--s-3); }

.block { display: flex; flex-direction: column; gap: var(--s-3); }

.block-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
.block-head-main { min-width: 0; }

/* The section heading gets a short accent rule in front of it — the cheapest possible way to make a
   page read as a sequence of topics rather than as one long field of cards. */
.block-title {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: var(--fs-lg);
  font-weight: 700;
  letter-spacing: -0.018em;
}
/* Three segments in the three brand colours — the cube's faces, unrolled. It is the cheapest
   possible way to make a page read as a sequence of topics *and* to put the brand on every
   section head without a single extra element. */
.block-title::before {
  content: '';
  flex: none;
  width: 3px; height: 0.86em;
  border-radius: 2px;
  background: linear-gradient(
    var(--coral) 0%, var(--coral) 33%,
    var(--purple-light) 33%, var(--purple-light) 66%,
    var(--ok) 66%, var(--ok) 100%);
}
.block-sub { margin-top: 3px; font-size: var(--fs-sm); color: var(--ink-3); max-width: 62ch; }
.block-actions { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--s-5);
  box-shadow: var(--lift);
}

.block--chart .card { padding: var(--s-5) var(--s-5) var(--s-3); }
.chart-card { display: flex; flex-direction: column; gap: var(--s-3); }
.chart-pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: var(--s-4); }

.chart-summary { display: flex; align-items: baseline; gap: var(--s-3); flex-wrap: wrap; margin-top: -2px; }
.chart-total { font-size: var(--fs-2xl); font-weight: 700; letter-spacing: -0.022em; }
.chart-total-sub { font-size: var(--fs-sm); color: var(--ink-3); }

/* ---------------------------------------------------------- stat tiles */

.stat-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(196px, 1fr));
  gap: var(--s-3);
}
/* The brief's three figures are small numbers: letting them stretch to 470px each made three
   nearly empty cards. They stop at a size the figure actually fills. */
.stat-strip--tight { grid-template-columns: repeat(auto-fit, minmax(140px, 220px)); }

.stat {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 132px;
  padding: var(--s-4) var(--s-5) var(--s-4);
  background:
    radial-gradient(120% 130% at 100% 0%, var(--stat-tint, rgba(106, 95, 211, 0.1)), transparent 62%),
    var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--lift);
  overflow: hidden;
}

/* The tile's own small drawing, in the top-right corner behind nothing. It is the *shape* of the
   figure, never a second copy of it, and it is `aria-hidden`. */
.stat-art {
  position: absolute;
  top: var(--s-3); right: var(--s-3);
  display: block;
  color: var(--ink-4);
  opacity: 0.85;
  pointer-events: none;
}
.stat--good .stat-art { color: var(--ok); opacity: 0.7; }
.stat--critical .stat-art { color: var(--crit); opacity: 0.7; }
.stat--warning .stat-art { color: var(--warn); opacity: 0.7; }
.tile-art { display: block; }
/* Room for the drawing: the label wraps before it runs under it. */
.stat-art + .stat-label { padding-right: 52px; }
/* A hairline of light along the top edge. One line of CSS, and it is the difference between a card
   that is a rectangle of a slightly different grey and one that is a physical thing. */
.stat::before,
.card::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1) 22%, rgba(255, 255, 255, 0.1) 78%, transparent);
  pointer-events: none;
}
.card { position: relative; overflow: hidden; }

.stat-label {
  display: block;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.07em;
  line-height: 1.35;
  text-transform: uppercase;
  color: var(--ink-3);
}

/* `margin-top: auto` pushes the figure to the bottom of the tile, so a Spanish label that wraps to
   two lines ("DISPONIBILIDAD, 30 DÍAS") no longer drops its own number a line below its neighbours'.
   Grid already makes the tiles equal height; this makes them share a baseline as well. */
.stat-value {
  margin-top: auto;
  padding-top: var(--s-2);
  font-size: var(--fs-3xl);
  font-weight: 800;
  letter-spacing: -0.032em;
  line-height: 1.05;
}

.stat-sub { margin-top: 4px; font-size: var(--fs-sm); color: var(--ink-3); }
.stat-sub--sev { display: flex; gap: var(--s-3); }

.sev-count { display: inline-flex; align-items: center; gap: 4px; font-variant-numeric: tabular-nums; }
/* The same chips in a site's row, so the table answers "does this one want me?" without the list
   underneath it. Icon (with its own title) plus a count — never the colour on its own. */
.row-flags { display: flex; align-items: center; gap: var(--s-2); flex: none; }
.row-flags .sev-count { font-size: var(--fs-sm); font-weight: 700; }

/* A tile that leads somewhere says so on hover, and nowhere else: a working tool should not
   decorate four cards with chevrons it does not need. */
a.stat--link {
  color: inherit;
  text-decoration: none;
  transition: border-color var(--t-fast) ease, background var(--t-fast) ease,
              transform var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
a.stat--link:hover {
  border-color: var(--line-2);
  background: var(--surface-2);
  color: inherit;
  transform: translateY(-1px);
  box-shadow: var(--lift-hover);
}
a.stat--link:active { transform: translateY(0); }
a.stat--link:hover .stat-label { color: var(--ink-2); }

.stat--good { --stat-tint: rgba(30, 168, 119, 0.12); }
.stat--good .stat-value { color: var(--ok); }
.stat--critical { border-color: rgba(242, 85, 90, 0.35); --stat-tint: rgba(242, 85, 90, 0.12); }
.stat--critical .stat-value { color: var(--crit); }
.stat--warning { border-color: rgba(240, 173, 60, 0.3); --stat-tint: rgba(240, 173, 60, 0.12); }
.stat--warning .stat-value { color: var(--warn); }

/* The overview's four. They were 132px tall each and they were the reason the sites table started
   below the fold — Marcelo opens this screen to see the sites. Same four facts, same identities,
   one line each: the drawing at the left, the label over the figure, the sub-line beside it. */
.stat-strip--slim { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s-3); }
.stat--slim {
  flex-direction: row;
  align-items: center;
  gap: var(--s-3);
  min-height: 0;
  padding: 10px var(--s-4);
}
.stat--slim .stat-art { position: static; flex: none; opacity: 0.8; }
.stat--slim .stat-main { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.stat--slim .stat-figure { display: flex; align-items: baseline; gap: var(--s-2); min-width: 0; }
.stat--slim .stat-value { margin-top: 0; padding-top: 0; font-size: var(--fs-xl); }
.stat--slim .stat-sub { margin-top: 0; min-width: 0; }
.stat--slim .stat-sub--sev { gap: var(--s-2); }

/* ------------------------------------------------------------ mock note --
   The full-width banner that used to sit under the hero is gone: it cost ~80px of the fold to
   repeat the rail's own MOCK DATA badge. What is left is `.hero-mock`, one line inside the hero,
   styled up with the rest of the hero. */

.notice {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: var(--s-3) var(--s-4);
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--bg-2);
  color: var(--ink-2);
  font-size: var(--fs-base);
}
.notice .icon { color: var(--ink-3); margin-top: 2px; flex: none; }
.notice--quiet { background: transparent; }
.notice--once {
  border-color: rgba(240, 173, 60, 0.35);
  background: var(--warn-soft);
  color: var(--warn);
  font-weight: 600;
}

/* --------------------------------------------------------- status marks */

.status { display: inline-flex; align-items: center; gap: 7px; font-size: var(--fs-base); font-weight: 600; white-space: nowrap; }
.status--up { color: var(--ok); }
.status--down { color: var(--crit); }
.status--unknown { color: var(--ink-3); }
.status--paused { color: var(--info); }

/* The one pulse in the app, so it cannot be read as anything but "this is down right now". It is a
   halo around the glyph rather than the glyph itself, so the shape stays legible at every frame. */
.status--down .icon {
  border-radius: 50%;
  animation: down-halo 2s ease-in-out infinite;
}
@keyframes down-halo {
  0%, 100% { box-shadow: 0 0 0 0 rgba(242, 85, 90, 0.45); }
  55% { box-shadow: 0 0 0 5px rgba(242, 85, 90, 0); }
}

.sev { display: inline-flex; align-items: center; gap: 7px; font-size: var(--fs-sm); font-weight: 600; }
.sev--critical { color: var(--crit); }
.sev--warning { color: var(--warn); }
.sev--info { color: var(--info); }

.pill {
  display: inline-flex;
  align-items: center;
  padding: 3px 11px;
  border-radius: 999px;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.015em;
  white-space: nowrap;
  border: 1px solid transparent;
}
.pill--neutral { background: rgba(255, 255, 255, 0.06); color: var(--ink-2); border-color: var(--line); }
.pill--good { background: var(--ok-soft); color: var(--ok); border-color: rgba(30, 168, 119, 0.3); }
.pill--warning { background: var(--warn-soft); color: var(--warn); border-color: rgba(240, 173, 60, 0.3); }
/* 14 % of the critical red under 11px ink measured 4.41:1 — just under. The ink is the thing that
   has to be read, so the tint gives way: 10 % takes the same pair to 4.68:1. Round 2's rule is
   untouched — status red is still a tint, never a filled action. */
.pill--critical { background: rgba(242, 85, 90, 0.1); color: var(--crit); border-color: rgba(242, 85, 90, 0.34); }
.pill--info { background: var(--info-soft); color: var(--info); border-color: rgba(138, 160, 204, 0.28); }

.delta { display: inline-flex; align-items: center; gap: 3px; font-size: var(--fs-sm); font-weight: 650; font-variant-numeric: tabular-nums; }
.delta--good { color: var(--ok); }
.delta--bad { color: var(--crit); }
.delta--flat { color: var(--ink-3); }

.days { font-variant-numeric: tabular-nums; font-size: var(--fs-base); white-space: nowrap; }
.days--warning { color: var(--warn); }
.days--critical { color: var(--crit); font-weight: 600; }

/* -------------------------------------------------------------- buttons */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  min-height: 36px;
  padding: 8px 17px;
  border-radius: 999px;
  border: 1px solid var(--line-2);
  background: transparent;
  color: var(--ink);
  font-size: var(--fs-base);
  font-weight: 650;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--t-fast) ease, border-color var(--t-fast) ease,
              transform var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.btn:hover { border-color: var(--line-3); background: rgba(255, 255, 255, 0.055); transform: translateY(-1px); }
.btn:active { transform: translateY(1px); }
.btn[disabled] { opacity: 0.45; cursor: not-allowed; transform: none; }
.btn[disabled]:hover { border-color: var(--line-2); background: transparent; }
.btn .icon { flex: none; }

/* Busy: the label stays (so the button keeps its width) and a spinner replaces the icon slot. */
.btn.is-busy { pointer-events: none; opacity: 0.8; }
.btn.is-busy::before {
  content: '';
  width: 12px; height: 12px;
  flex: none;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-top-color: transparent;
  opacity: 0.85;
  animation: spin 720ms linear infinite;
}
.btn.is-busy .icon { display: none; }

/* Coral is reserved for the one primary action on a view, and only ever as a filled pill.
   --coral-strong, not --coral: see the token block for why. */
.btn--primary {
  background: var(--coral-strong);
  border-color: var(--coral-strong);
  color: #fff;
  box-shadow: 0 8px 22px -10px var(--coral-glow);
}
.btn--primary:hover { background: var(--coral-press); border-color: var(--coral-press); box-shadow: 0 12px 28px -10px var(--coral-glow); }
.btn--primary:active { background: var(--coral-press); }

.btn--secondary { background: var(--purple); border-color: var(--purple); color: #fff; }
.btn--secondary:hover { background: var(--purple-light); border-color: var(--purple-light); }

.btn--danger { background: transparent; border-color: rgba(242, 85, 90, 0.5); color: var(--crit); }
.btn--danger:hover { background: var(--crit-soft); border-color: var(--crit); }

.btn--danger-ghost { border-color: var(--line-2); color: var(--ink-2); }
.btn--danger-ghost:hover { border-color: rgba(242, 85, 90, 0.55); color: var(--crit); background: var(--crit-soft); }

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px; height: 38px;
  border-radius: var(--radius);
  border: 1px solid transparent;
  background: transparent;
  color: var(--ink-3);
  cursor: pointer;
}
.icon-btn:hover { color: var(--ink); background: rgba(255, 255, 255, 0.07); }

.segmented { display: inline-flex; gap: 2px; padding: 3px; background: rgba(255, 255, 255, 0.05); border-radius: 999px; }
.seg {
  border: 0;
  background: transparent;
  color: var(--ink-3);
  font-size: var(--fs-sm);
  font-weight: 700;
  padding: 7px 15px;
  border-radius: 999px;
  cursor: pointer;
}
.seg[aria-pressed='true'] { background: var(--surface-2); color: var(--ink); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); }
.seg:hover[aria-pressed='false'] { color: var(--ink-2); }

.subtle-link { color: var(--ink-2); }
.subtle-link:hover { color: var(--purple-light); }
.plain-link { color: var(--ink); }
.plain-link:hover { color: var(--purple-light); }

/* ---------------------------------------------------------- flag list */

.flag-list { display: flex; flex-direction: column; gap: 5px; }

.flag-row {
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr) auto 18px;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--lift);
  transition: border-color var(--t-fast) ease, background var(--t-fast) ease,
              transform var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
a.flag-row:hover, button.flag-row:hover {
  border-color: var(--line-2);
  background: var(--surface-2);
  color: var(--ink);
  transform: translateY(-1px);
  box-shadow: var(--lift-hover);
}
a.flag-row:active, button.flag-row:active { transform: translateY(0); }
a.flag-row:hover .flag-chevron { transform: translateX(3px); color: var(--ink-2); }

/* Three tiers, not one. Round 1 made the ranking correct; it was invisible because every row had the
   same weight and only a 2px edge to tell them apart. Critical gets a wash and a heavier line,
   warning keeps the plain card, info drops out of the card shape altogether. */
/* The wash is deliberately faint. At four sites two red rows among six look right at full strength;
   at 120 there are nineteen of them and a saturated list is a wall, not a ranking. The edge and the
   heavier first line do the separating; the tint only agrees with them. */
.flag-row--critical {
  border-left: 3px solid var(--crit);
  background: linear-gradient(90deg, rgba(242, 85, 90, 0.075), transparent 52%), var(--surface);
  border-color: rgba(242, 85, 90, 0.22);
  border-left-color: var(--crit);
  padding-top: var(--s-4);
  padding-bottom: var(--s-4);
}
.flag-row--critical .flag-text { font-size: 17px; font-weight: 700; letter-spacing: -0.012em; }
a.flag-row--critical:hover { border-color: rgba(242, 85, 90, 0.5); border-left-color: var(--crit); }

.flag-row--warning { border-left: 3px solid var(--warn); }

.flag-row--info {
  border-color: transparent;
  background: transparent;
  box-shadow: none;
  padding: var(--s-2) var(--s-4) var(--s-2) var(--s-3);
  border-left: 3px solid rgba(138, 160, 204, 0.4);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.flag-row--info .flag-text { font-size: var(--fs-base); color: var(--ink-2); font-weight: 550; }
a.flag-row--info:hover, button.flag-row--info:hover { box-shadow: none; transform: none; }
a.flag-row--info:hover, button.flag-row--info:hover { background: rgba(255, 255, 255, 0.035); border-color: transparent; border-left-color: var(--info); }

/* The quiet tier sits under a hairline so it reads as a footnote to the loud one. */
.flag-list--quiet {
  margin-top: 6px;
  padding-top: 8px;
  border-top: 1px solid var(--line);
}

.flag-sev { display: inline-flex; }
.flag-main { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.flag-text { font-size: var(--fs-md); font-weight: 600; line-height: 1.4; letter-spacing: -0.008em; }
.flag-meta { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: var(--fs-sm); color: var(--ink-3); }
.flag-site { font-weight: 650; color: var(--ink-2); }
.flag-age { font-size: var(--fs-sm); color: var(--ink-3); white-space: nowrap; font-variant-numeric: tabular-nums; }
.flag-chevron { color: var(--ink-4); transition: transform var(--t) var(--ease), color var(--t) ease; }
.flag-chevron--open { transform: rotate(90deg); }
.flag-list--plain .flag-row { grid-template-columns: 26px minmax(0, 1fr) auto; }

/* A collapsed group of identical info-level flags, and the rows it opens. */
.attention { display: flex; flex-direction: column; gap: 10px; }
.attention-filter { display: flex; flex-wrap: wrap; gap: 12px; }
.attention-more { display: flex; justify-content: center; }
button.flag-group { width: 100%; text-align: left; font: inherit; cursor: pointer; }
button.flag-group:hover { border-color: var(--line-2); background: var(--surface-2); }
.flag-nested { padding-left: 18px; }
.flag-nested .flag-row { background: var(--surface-2); }

@media (prefers-reduced-motion: reduce) {
  .flag-chevron { transition: none; }
}

/* All clear is the state Marcelo most wants to see, so it is built as a reward rather than as an
   absence: the sentence, plus the two facts that make it believable — how many sites are watched
   and when they were last looked at. */
/* Round 2 made this believable (the sentence plus the two facts behind it). Round 4 makes it a
   reward: the drawing is the size of the thing you were hoping to see, and it is the only place in
   the hub where the illustration is bigger than the sentence beside it. */
.all-clear {
  display: flex;
  align-items: center;
  gap: var(--s-6);
  padding: var(--s-6) var(--s-6);
  border-radius: var(--radius-xl);
  border: 1px solid rgba(30, 168, 119, 0.26);
  background:
    radial-gradient(90% 160% at 8% 0%, rgba(30, 168, 119, 0.15), transparent 60%),
    var(--surface);
  box-shadow: var(--lift);
}

.all-clear-art { flex: none; display: block; color: var(--ok); }

.all-clear-text { min-width: 0; display: flex; flex-direction: column; gap: 5px; }
.all-clear-title { font-size: var(--fs-xl); font-weight: 700; color: var(--ok); letter-spacing: -0.02em; line-height: 1.2; }
.all-clear-sub { font-size: var(--fs-base); color: var(--ink-3); }

/* -------------------------------------------------------------- tables */

.table-wrap {
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--lift);
}

/* 14px, not 15: the table is the one place where density is the feature, and the 28px mark plus the
   larger site name already give the row its weight. Four rows measured 232px before this round and
   268px after, so a 900px viewport still shows about nineteen. */
.data-table { width: 100%; border-collapse: collapse; font-size: 14px; }

.data-table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface);
  text-align: left;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.055em;
  text-transform: uppercase;
  color: var(--ink-3);
  padding: 11px 12px;
  border-bottom: 1px solid var(--line-2);
  white-space: nowrap;
}
.data-table thead th.num { text-align: right; }

/* The sites table is wide: fix the layout so the columns keep their sizes, and let the
   column names wrap instead of being clipped (Spanish headers are longer). */
.data-table--fixed { table-layout: fixed; }
.data-table.data-table--fixed thead th { white-space: normal; line-height: 1.22; vertical-align: bottom; }
.data-table.data-table--fixed td,
.data-table.data-table--fixed th { padding-left: 10px; padding-right: 10px; }

.th-sort {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  padding: 0;
  cursor: pointer;
}
.th-sort:hover { color: var(--ink); }
.th-arrow { color: var(--purple-light); font-size: var(--fs-xs); width: 10px; }
.data-table thead th.num .th-sort { flex-direction: row-reverse; }

.data-table td {
  padding: 8px 12px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
  vertical-align: middle;
}
.data-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
.data-table tbody tr:last-child td { border-bottom: 0; }

.data-table .row--link { cursor: pointer; transition: background 100ms ease; }
.data-table .row--link:hover { background: rgba(106, 95, 211, 0.1); }
.data-table .row--link:hover .site-name { color: #fff; }
.data-table .row--link:focus-visible { outline: 2px solid var(--purple-light); outline-offset: -2px; }

.data-table--compact { font-size: var(--fs-sm); }
.data-table--compact td, .data-table--compact th { padding: 7px 10px; }

.site-cell { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.site-name { font-weight: 700; font-size: var(--fs-base); color: var(--ink); letter-spacing: -0.008em; }
.site-host { display: flex; align-items: center; gap: var(--s-3); font-size: var(--fs-sm); color: var(--ink-3); min-width: 0; }
.site-host-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.resp-cell { display: inline-flex; align-items: center; gap: 9px; justify-content: flex-end; }
.resp-value { font-variant-numeric: tabular-nums; white-space: nowrap; }

.num-cell { display: inline-flex; flex-direction: column; align-items: flex-end; gap: 0; line-height: 1.3; }
.num-value { font-variant-numeric: tabular-nums; font-weight: 650; }

.change-cell { display: flex; flex-direction: column; gap: 0; line-height: 1.3; min-width: 0; }
.change-when { font-size: var(--fs-sm); color: var(--ink-2); white-space: nowrap; }
.change-title { font-size: var(--fs-xs); color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }

.alert-text { display: block; overflow: hidden; text-overflow: ellipsis; }

/* ------------------------------------------------------------- filters */

.filter-bar {
  display: flex;
  align-items: flex-end;
  gap: 10px;
  flex-wrap: wrap;
  padding: 12px 14px;
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.filter-tail { display: flex; align-items: center; gap: var(--s-3); margin-left: auto; }
.filter-count { font-size: var(--fs-sm); color: var(--ink-3); font-variant-numeric: tabular-nums; white-space: nowrap; }

.field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.field--inline { gap: 4px; }
.field--wide { grid-column: 1 / -1; }

.field-label { font-size: var(--fs-sm); font-weight: 650; letter-spacing: 0.02em; color: var(--ink-3); display: inline-flex; gap: 4px; }
/* A plain asterisk, not an alarm: coral is filled-surface-only and red means "something is wrong". */
.req { color: var(--ink-3); }
.field-hint { font-size: var(--fs-sm); color: var(--ink-3); line-height: 1.5; }
/* A hint that is a consequence rather than a tip: what choosing the non-default option opens up. */
.field-hint--warn {
  border-left: 2px solid var(--warn);
  padding-left: 10px;
  color: var(--ink-2);
}

.field-input {
  width: 100%;
  min-height: 38px;
  padding: 8px 12px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--line-2);
  border-radius: var(--radius-sm);
  color: var(--ink);
  font-size: var(--fs-base);
  transition: border-color 120ms ease, background 120ms ease;
}
.field-input:hover { border-color: var(--line-3); }
/* The glow stays, but it is a *glow*: 3px of purple at low alpha on a dark input measured 1.2:1
   against the input behind it, which is not a focus indicator. The ring from :focus-visible is what
   carries WCAG 1.4.11; the glow is the flourish around it. */
.field-input:focus { border-color: var(--purple-light); background: rgba(255, 255, 255, 0.055); box-shadow: 0 0 0 3px var(--purple-glow); }
.field-input--area { resize: vertical; line-height: 1.55; }
.field-input--mono { font-family: var(--mono); font-size: var(--fs-sm); }

select.field-input { appearance: none; padding-right: 30px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23999'%3E%3Cpath d='M4 6.5L8 10.5L12 6.5' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 9px center; background-size: 15px; }

.field--search { min-width: 220px; flex: 1 1 220px; max-width: 340px; }
.search-input { position: relative; display: flex; align-items: center; }
.search-input .icon { position: absolute; left: 11px; color: var(--ink-3); pointer-events: none; }
.search-input .field-input { padding-left: 35px; }

.field--switch { gap: 4px; }
.switch { display: inline-flex; align-items: center; gap: 9px; cursor: pointer; }
.switch-input { position: absolute; opacity: 0; width: 0; height: 0; }
.switch-track {
  width: 34px; height: 19px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid var(--line-2);
  display: inline-flex;
  align-items: center;
  padding: 2px;
  transition: background 140ms ease;
  flex: none;
}
.switch-knob { width: 13px; height: 13px; border-radius: 50%; background: var(--ink-2); transition: transform 140ms ease, background 140ms ease; }
.switch-input:checked + .switch-track { background: var(--purple); border-color: var(--purple); }
.switch-input:checked + .switch-track .switch-knob { transform: translateX(15px); background: #fff; }
.switch-input:focus-visible + .switch-track { outline: 2px solid var(--purple-light); outline-offset: 2px; }
.switch .field-label { color: var(--ink-2); font-size: var(--fs-base); }

.switch-row { display: flex; flex-direction: row; gap: 28px; flex-wrap: wrap; }

.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-4) var(--s-4); }
.form-grid--one { grid-template-columns: minmax(0, 1fr); }
.form-error { color: var(--crit); font-size: var(--fs-base); margin-top: var(--s-3); }
.form-error-slot:empty { display: none; }

.color-row { display: flex; align-items: center; gap: 8px; }
.color-input { width: 34px; height: 32px; padding: 2px; border: 1px solid var(--line-2); border-radius: var(--radius-sm); background: transparent; cursor: pointer; flex: none; }

/* ---------------------------------------------------------- empty/state */

/* The default empty state is a sentence at the height of a row, not a 100px box. "No incidents in
   the last 90 days" is good news and should occupy as much space as good news deserves. */
.empty {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  flex-wrap: wrap;
  padding: var(--s-4) var(--s-4);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-2);
  color: var(--ink-3);
  font-size: var(--fs-base);
  line-height: 1.5;
}
.empty p { max-width: var(--measure-wide); }
/* The drawing goes in the empty state, never beside data — round 4's first rule. It stays at
   --ink-4 with one accent stroke, so it reads as furniture rather than as a second headline. */
.empty-art { flex: none; display: block; color: var(--ink-4); }

/* The one case where something is genuinely missing and there is a whole screen to say so in. */
.empty--page {
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--s-4);
  padding: var(--s-8) var(--s-5);
  background: transparent;
  border-style: dashed;
}
.empty--page .empty-art { margin-bottom: var(--s-1); }

.loading { display: flex; align-items: center; gap: var(--s-3); padding: var(--s-7) var(--s-1); color: var(--ink-3); font-size: var(--fs-base); }
.spinner {
  width: 16px; height: 16px;
  border-radius: 50%;
  border: 2px solid var(--line-2);
  border-top-color: var(--purple-light);
  animation: spin 720ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.error-box {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-5);
  border-radius: var(--radius-lg);
  border: 1px solid rgba(242, 85, 90, 0.35);
  background: var(--crit-soft);
}
.error-box .empty-art { color: var(--crit); }
.error-title { font-weight: 700; font-size: var(--fs-md); }
.error-detail { font-size: var(--fs-base); color: var(--ink-2); margin-top: 3px; }
.error-box .btn { margin-left: auto; }

.boot-error { padding: var(--s-8); color: var(--ink-2); font-size: var(--fs-md); }

/* ------------------------------------------------------------ not found */

.not-found {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s-4);
  max-width: 46ch;
  padding: var(--s-6) 0 var(--s-8);
}
.not-found .empty-art { margin-bottom: var(--s-1); color: var(--ink-3); }
.not-found-title { font-size: var(--fs-3xl); font-weight: 800; letter-spacing: -0.028em; line-height: 1.08; }
.not-found-body { font-size: var(--fs-md); color: var(--ink-2); line-height: 1.6; }
.not-found-path {
  font-family: var(--mono);
  font-size: var(--fs-sm);
  color: var(--ink-3);
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 6px 10px;
  max-width: 100%;
  overflow-wrap: anywhere;
}

/* --------------------------------------------------------- first paint */
/* Seeding the mock world takes a couple of seconds on a cold database. A bare spinner with the
   word "Loading" tells the reader nothing; this says what is happening and holds the layout. */

.boot { display: flex; flex-direction: column; gap: var(--s-5); padding: var(--s-2) 0 var(--s-6); }
.boot-head { display: flex; flex-direction: column; gap: var(--s-1); }
.boot-title { font-size: var(--fs-xl); font-weight: 700; letter-spacing: -0.02em; }
.boot-note { font-size: var(--fs-base); color: var(--ink-3); max-width: var(--measure); }

.sk {
  border-radius: var(--radius);
  background: linear-gradient(90deg, rgba(255, 255, 255, 0.04) 25%, rgba(255, 255, 255, 0.08) 37%, rgba(255, 255, 255, 0.04) 63%);
  background-size: 400% 100%;
  animation: sk-shimmer 1.6s ease infinite;
}
.sk--tiles { height: 92px; }
.sk--row { height: 46px; }
.sk--block { height: 220px; }
.sk-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(178px, 1fr)); gap: var(--s-3); }
.sk-rows { display: flex; flex-direction: column; gap: var(--s-1); }
@keyframes sk-shimmer { from { background-position: 100% 0; } to { background-position: 0 0; } }

/* ---------------------------------------------------------- code blocks */

.copy-block {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 12px;
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.copy-label { font-size: var(--fs-xs); color: var(--ink-3); padding-top: 3px; }
.code-block {
  flex: 1;
  min-width: 0;
  font-family: var(--mono);
  font-size: var(--fs-sm);
  line-height: 1.6;
  color: var(--ink-2);
  white-space: pre-wrap;
  word-break: break-all;
}
.copy-block .btn { flex: none; }

/* The three install steps beside the connector's code. Numbered on purpose: they are done in
   order, on somebody else's server, and the order is the whole instruction. */
.steps {
  list-style: decimal;
  padding-left: 1.4em;
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: var(--fs-sm);
  color: var(--ink-2);
}
.steps li { padding-left: 2px; }

/* ----------------------------------------------------------- disclosure */

/* The "Advanced" fold on the site page's connector section: the paste-it-by-hand path, present and
   reachable but not competing with the one button that installs the plugin. */

.disclosure {
  border-top: 1px solid var(--line);
  padding-top: var(--s-3);
  margin-top: var(--s-2);
}

.disclosure-summary {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  font-size: var(--fs-sm);
  color: var(--ink-2);
  padding: 2px 4px;
  margin-left: -4px;
  list-style: none;
  border-radius: var(--radius-sm);
}

.disclosure-summary::-webkit-details-marker { display: none; }
.disclosure-summary:hover { color: var(--ink); }
.disclosure-summary:focus-visible { outline: 2px solid var(--purple-light); outline-offset: 2px; }
.disclosure-summary .icon { transform: rotate(90deg); transition: transform 140ms ease; }
.disclosure[open] .disclosure-summary .icon { transform: rotate(-90deg); }
.disclosure-body { padding-top: var(--s-3); }

@media (prefers-reduced-motion: reduce) {
  .disclosure-summary .icon { transition: none; }
}

/* ------------------------------------------------------------- timeline */

.timeline { display: flex; flex-direction: column; }

.timeline-item {
  display: grid;
  grid-template-columns: 90px 14px minmax(0, 1fr);
  gap: 12px;
  padding: 9px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.045);
}
.timeline-item:last-child { border-bottom: 0; }

.timeline-when { font-size: var(--fs-sm); color: var(--ink-3); padding-top: 2px; font-variant-numeric: tabular-nums; }
.timeline-dot { width: 7px; height: 7px; border-radius: 50%; margin-top: 7px; background: var(--ink-4); justify-self: center; }
.timeline-dot--deploy { background: var(--viz-series-1); }
.timeline-dot--update { background: var(--info); }
.timeline-dot--note { background: var(--purple-light); }

.timeline-title { font-size: var(--fs-base); font-weight: 550; }
.timeline-detail { font-size: var(--fs-sm); color: var(--ink-2); margin-top: 2px; }
.timeline-meta { display: flex; gap: 6px; font-size: var(--fs-xs); color: var(--ink-3); margin-top: 3px; }

/* ------------------------------------------------------------ wordpress */

.wp-head { display: flex; align-items: center; gap: 24px; flex-wrap: wrap; padding: 4px 2px 10px; }
.wp-fact { display: inline-flex; align-items: center; gap: 8px; }
.wp-fact-label { font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3); }
.wp-fact-value { font-size: var(--fs-lg); font-weight: 700; font-variant-numeric: tabular-nums; }
.wp-sync { font-size: var(--fs-sm); color: var(--ink-3); margin-left: auto; }

.wp-item { display: flex; align-items: baseline; gap: 8px; }
.wp-item-name { font-weight: 600; }
.wp-vuln { display: flex; align-items: center; gap: 8px; min-width: 0; }
.wp-vuln-text { font-size: var(--fs-sm); color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* -------------------------------------------------------------- reports */

.report-list { display: flex; flex-direction: column; gap: 10px; }

.report-row {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr) auto;
  gap: var(--s-5);
  align-items: start;
  padding: var(--s-5);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--lift);
  transition: border-color var(--t-fast) ease, box-shadow var(--t-fast) var(--ease);
}
.report-row:hover { border-color: var(--line-2); box-shadow: var(--lift-hover); }

.report-main { min-width: 0; display: flex; flex-direction: column; gap: 5px; }
.report-head { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; }
.report-client { font-size: var(--fs-xl); font-weight: 700; letter-spacing: -0.02em; }
.report-subject { font-size: var(--fs-sm); color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; }

/* One quiet line where four uppercase labels used to be. */
.report-meta {
  margin-top: var(--s-2);
  font-size: var(--fs-sm);
  line-height: 1.6;
  color: var(--ink-3);
  overflow-wrap: anywhere;
}
.report-meta-arrow { color: var(--ink-4); }

.report-note { min-width: 0; display: flex; flex-direction: column; gap: 5px; }
/* A note preview that is one line in one card and three in the next leaves the row ragged. */
.report-row .note-preview { min-height: 62px; }

/* Compact mode: one line per client, the note editor folded away until the row is opened. */
.report-list--compact { gap: 4px; }

.report-line {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}
.report-line--open { border-color: var(--line-2); }
.report-line-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px; padding-right: 10px; }

button.report-line-main {
  display: grid;
  grid-template-columns: 20px minmax(120px, 1.1fr) auto minmax(60px, auto) minmax(0, 2fr) 16px;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 10px 14px;
  text-align: left;
  font: inherit;
  color: var(--ink);
  background: none;
  border: 0;
  cursor: pointer;
}
button.report-line-main:hover { background: var(--surface-2); }

.report-line-client { font-size: var(--fs-base); font-weight: 600; }
.report-line-writer, .report-line-note { font-size: var(--fs-sm); }
.report-line-note { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink-3); }
.report-line-chevron { color: var(--ink-4); transition: transform 140ms ease; }
.report-line--open .report-line-chevron { transform: rotate(90deg); }
.report-line .report-note:not(:empty) { padding: 0 14px 12px; border-top: 1px solid var(--line); padding-top: 12px; }

@media (max-width: 900px) {
  button.report-line-main { grid-template-columns: 20px minmax(0, 1fr) auto 16px; }
  .report-line-writer, .report-line-note { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .report-line-chevron { transition: none; }
}
.note-label { font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase; color: var(--ink-3); }
.note-preview {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  text-align: left;
  width: 100%;
  padding: 8px 10px;
  background: rgba(255, 255, 255, 0.03);
  border: 1px dashed var(--line-2);
  border-radius: var(--radius-sm);
  color: var(--ink-2);
  font-size: var(--fs-sm);
  line-height: 1.5;
  cursor: pointer;
}
.note-preview:hover { border-color: var(--purple-light); color: var(--ink); }
.note-preview span { flex: 1; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.note-preview .icon { color: var(--ink-3); flex: none; margin-top: 2px; }
.note-actions { display: flex; gap: 8px; }

.report-actions { display: flex; flex-direction: column; gap: 8px; align-items: stretch; }

.meta-bar {
  display: flex;
  gap: 26px;
  flex-wrap: wrap;
  padding: 12px 16px;
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.meta-item dt { font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase; color: var(--ink-3); }
.meta-item dd { font-size: var(--fs-base); margin-top: 1px; }

/* The preview used to stop dead at 760px with the email cut through a table and no scrollbar, which
   read as broken rather than as "there is more". Taller, and the cut is now a deliberate fade with
   the frame's own scrollbar behind it. */
.mail-stage {
  position: relative;
  display: flex;
  justify-content: center;
  padding: var(--s-5);
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}
.mail-stage::after {
  content: '';
  position: absolute;
  left: 1px; right: 1px; bottom: 1px;
  height: 28px;
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  background: linear-gradient(to top, var(--bg-2), transparent);
  pointer-events: none;
}
.mail-frame {
  width: 100%;
  height: min(1080px, 78vh);
  min-height: 560px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  background: #fff;
  transition: max-width var(--t) var(--ease);
}
/* 700px, not 860: the email itself is 600px wide, and a 260px moat of stage around it made the
   preview look like a slide rather than an inbox. */
.mail-stage--desktop .mail-frame { max-width: 700px; }
.mail-stage--mobile .mail-frame { max-width: 390px; }

.prose-card { display: flex; flex-direction: column; gap: 8px; }
.prose-head { font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3); margin-top: 6px; }
.prose-head:first-child { margin-top: 0; }
.prose-text { font-size: var(--fs-md); line-height: 1.65; color: var(--ink-2); max-width: 74ch; }
.prose-list { display: flex; flex-direction: column; gap: 4px; }
.prose-list li { position: relative; padding-left: 16px; font-size: var(--fs-base); color: var(--ink-2); }
.prose-list li::before { content: ''; position: absolute; left: 3px; top: 9px; width: 5px; height: 5px; border-radius: 50%; background: var(--purple-light); }

.brief-note {
  margin: 0;
  padding: 12px 16px;
  border-left: 2px solid var(--purple);
  background: rgba(106, 95, 211, 0.08);
  border-radius: 0 var(--radius) var(--radius) 0;
  font-size: var(--fs-base);
  color: var(--ink-2);
}
/* `auto-fit` with a 540px cap left a lone card stranded at half width with 600px of empty page
   beside it. One site gets the full measure; two or more share the row. */
.brief-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s-3); }
.brief-grid--many { grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); }
.brief-grid--many > .brief-card { max-width: 620px; }
.brief-card { display: flex; flex-direction: column; gap: 10px; }
.brief-title { font-size: var(--fs-md); font-weight: 700; }
.brief-url { font-size: var(--fs-sm); color: var(--ink-3); margin-top: -8px; }
.brief-metrics { display: flex; flex-direction: column; }
/* Value and deltas on one line. They used to wrap onto a second row, so a metric with a comparison
   was twice the height of one without and the column had no rhythm at all. */
.metric-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3);
  flex-wrap: wrap;
  padding: 6px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.045);
  font-size: var(--fs-base);
}
.metric-label { flex: 1 1 auto; min-width: 0; }
.metric-row:last-child { border-bottom: 0; }
.metric-label { color: var(--ink-3); }
.metric-value { font-weight: 650; order: 2; min-width: 68px; text-align: right; }
.metric-delta { display: flex; gap: var(--s-3); justify-content: flex-end; flex-wrap: wrap; order: 3; }
.metric-delta-pair { display: inline-flex; align-items: baseline; gap: 4px; }
.metric-delta-note { font-size: var(--fs-xs); color: var(--ink-3); }
.brief-lists { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; }
.brief-sub { font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase; color: var(--ink-3); margin-bottom: 4px; }
.brief-list ol { display: flex; flex-direction: column; gap: 2px; }
.brief-list li { display: flex; justify-content: space-between; gap: 8px; font-size: var(--fs-sm); }
.brief-list-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink-2); }
.brief-change-list { display: flex; flex-direction: column; gap: 3px; }
.brief-change-list li { display: flex; gap: 8px; font-size: var(--fs-sm); }

/* ---------------------------------------------------------- connections */

.conn-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 12px; }
.conn-card { display: flex; flex-direction: column; gap: 9px; }
.conn-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.conn-name { font-size: var(--fs-md); font-weight: 700; font-family: var(--mono); letter-spacing: -0.01em; }
.conn-state-help { font-size: var(--fs-sm); color: var(--ink-3); }
.conn-facts { display: flex; flex-direction: column; gap: 8px; }
.conn-facts dt { font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase; color: var(--ink-3); }
.conn-facts dd { font-size: var(--fs-base); color: var(--ink-2); line-height: 1.5; margin-top: 1px; }
.conn-file { display: flex; align-items: center; gap: 6px; margin-top: auto; padding-top: 6px; border-top: 1px solid var(--line); color: var(--ink-3); }
.conn-file code { font-family: var(--mono); font-size: var(--fs-xs); }
.conn-facts dd code { font-family: var(--mono); font-size: var(--fs-sm); }

/* A field and the button that saves it, side by side; they stack on a phone rather than squeeze. */
.inline-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.inline-row .field-input { flex: 1 1 14rem; min-width: 0; }

.dev-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  padding: 11px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.dev-row:last-child { border-bottom: 0; }
.dev-title { font-size: var(--fs-base); font-weight: 600; }
.dev-help { font-size: var(--fs-sm); color: var(--ink-3); margin-top: 1px; max-width: 64ch; }

.block--danger .danger-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding: 14px 16px;
  border: 1px solid rgba(242, 85, 90, 0.26);
  border-radius: var(--radius-lg);
  background: rgba(242, 85, 90, 0.05);
}
.danger-title { font-size: var(--fs-base); font-weight: 650; }
.danger-body { font-size: var(--fs-sm); color: var(--ink-3); margin-top: 2px; max-width: 70ch; }

/* --------------------------------------------------------------- logos */

.logo-row { display: flex; align-items: center; gap: var(--s-3); }
.logo-previews { display: flex; gap: var(--s-3); margin-top: var(--s-2); flex-wrap: wrap; }
/* Round 4 §4: a brand logo shown at 150 × 74 with a 9.5px caption is a thumbnail of an identity.
   These are the only two surfaces in the hub where the studio's own mark is on display, so they are
   shown at something close to the size the email masthead actually prints them. */
.logo-preview {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 230px; height: 108px;
  border-radius: var(--radius-lg);
  border: 1px solid var(--line-2);
  padding: var(--s-4) var(--s-5) var(--s-6);
  box-shadow: var(--lift);
}
.logo-preview img { max-width: 100%; max-height: 100%; object-fit: contain; }
.logo-preview--dark { background: #0c0c10; }
.logo-preview--light { background: #ffffff; }
.logo-preview-label {
  position: absolute;
  bottom: 7px; left: 0; right: 0;
  text-align: center;
  font-size: var(--fs-xs);
  font-weight: 650;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}
/* These two sit on the only white surface in the hub, so they get their own ink rather than the
   UI's. At 9.5px they are small text, which means 4.5:1: 45 % black on white measured 3.36:1 and
   50 % white on the dark plate 5.0 — the light one is the one that had to move. */
.logo-preview--dark .logo-preview-label { color: rgba(255, 255, 255, 0.62); }
.logo-preview--light .logo-preview-label { color: rgba(0, 0, 0, 0.66); }
.logo-slot { margin-top: 6px; display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }

/* -------------------------------------------------------------- dialogs */

.dialog {
  border: 1px solid var(--line-2);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--ink);
  padding: 0;
  width: min(560px, calc(100vw - 32px));
  max-height: min(86vh, 760px);
  box-shadow: 0 30px 80px -20px rgba(0, 0, 0, 0.8);
}
.dialog::backdrop { background: rgba(6, 6, 9, 0.68); backdrop-filter: blur(2px); }
.dialog--confirm { width: min(440px, calc(100vw - 32px)); }
.dialog--form { width: min(620px, calc(100vw - 32px)); }
.dialog--search { width: min(560px, calc(100vw - 32px)); }

.dialog-form { display: flex; flex-direction: column; max-height: inherit; }
.dialog-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 16px 12px; }
.dialog-title { font-size: var(--fs-lg); font-weight: 700; letter-spacing: -0.015em; }
.dialog-body { padding: 0 16px 16px; overflow: auto; }
.dialog-text { font-size: var(--fs-base); color: var(--ink-2); line-height: 1.6; }
.dialog-foot { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--line); }

.qs { display: flex; flex-direction: column; gap: 10px; }
.qs-input {
  width: 100%;
  padding: 9px 12px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--line-2);
  border-radius: var(--radius-sm);
  font-size: var(--fs-md);
}
.qs-input:focus { border-color: var(--purple-light); box-shadow: 0 0 0 3px var(--purple-glow); }
.qs-list { max-height: 320px; overflow: auto; display: flex; flex-direction: column; gap: 1px; }
.qs-group { font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3); padding: 9px 8px 3px; }
.qs-item {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 7px 10px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: var(--fs-base);
}
.qs-item.is-active { background: rgba(106, 95, 211, 0.22); }
.qs-item:hover { background: rgba(255, 255, 255, 0.05); }
.qs-name { font-weight: 550; }
.qs-hint { font-size: var(--fs-sm); color: var(--ink-3); }
.qs-empty { padding: 18px 8px; color: var(--ink-3); font-size: var(--fs-base); text-align: center; }
.qs-foot { font-size: var(--fs-xs); color: var(--ink-3); border-top: 1px solid var(--line); padding-top: 8px; }

/* --------------------------------------------------------------- toasts */

.toasts {
  position: fixed;
  z-index: 60;
  bottom: 18px; right: 18px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  pointer-events: none;
}
.toast {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 10px 14px;
  border-radius: var(--radius);
  background: var(--surface-2);
  border: 1px solid var(--line-2);
  box-shadow: 0 14px 40px -12px rgba(0, 0, 0, 0.7);
  font-size: var(--fs-base);
  pointer-events: auto;
  max-width: 380px;
  animation: toast-in 180ms ease;
}
.toast--ok .icon { color: var(--ok); }
.toast--error { border-color: rgba(242, 85, 90, 0.4); }
.toast--error .icon { color: var(--crit); }
/* A toast that can only be dismissed by clicking it cannot be dismissed at all without a pointer. */
.toast-close {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px; height: 24px;
  margin: -4px -6px -4px 2px;
  border-radius: 999px;
  color: var(--ink-3);
}
.toast-close:hover { color: var(--ink); background: rgba(255, 255, 255, 0.07); }

.toast.is-leaving { opacity: 0; transform: translateY(6px); transition: opacity 180ms ease, transform 180ms ease; }
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } }

/* ---------------------------------------------------------------- login */

.login-screen {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: var(--s-6);
  /* One bloom behind the card, so the front door is not a grey box on a black field. */
  background: radial-gradient(60% 45% at 50% 22%, rgba(106, 95, 211, 0.16), transparent 70%);
}
.login-card {
  position: relative;
  width: min(420px, 100%);
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: var(--s-7);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-xl);
  box-shadow: 0 40px 90px -40px rgba(0, 0, 0, 0.9), var(--lift);
}
.login-art-slot { display: flex; justify-content: center; color: var(--purple-light); opacity: 0.85; margin-bottom: var(--s-2); }
.login-brand { display: flex; align-items: center; gap: var(--s-3); }
.login-wordmark { font-size: var(--fs-xl); font-weight: 800; letter-spacing: -0.02em; }
.login-title { font-size: var(--fs-3xl); font-weight: 800; letter-spacing: -0.02em; margin-top: 10px; }
.login-body { font-size: var(--fs-base); color: var(--ink-3); }
.login-form { display: flex; flex-direction: column; gap: 14px; margin-top: 8px; }
.login-form .btn { width: 100%; }

/* ----------------------------------------------------------- responsive */

@media (max-width: 1180px) {
  .report-row { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .report-actions { grid-column: 1 / -1; flex-direction: row; justify-content: flex-end; }
}

/* The hero stays a row at every width. Stacking it put the cube on a line of its own and cost
   ~170px of a phone screen, which is the opposite of what this screen now has to do; the cube
   shrinks instead. */
@media (max-width: 1100px) {
  .hero { gap: var(--s-4); padding: var(--s-4) var(--s-5); }
  .hero { --cube-size: 106px; }
}
/* Four across while four fit; below that a 2×2 that is still shorter than the old single row. */
@media (max-width: 1150px) {
  .stat-strip--slim { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .hero { padding: var(--s-4); gap: var(--s-3); --cube-size: 86px; }
  .hero-text { font-size: 20px; }
  /* Tighter blocks on a phone: the sites list has to start as close to the first screen as the
     hero and the four figures above it allow. */
  .stack { gap: var(--s-5); }
  .stat--slim { padding: var(--s-2) var(--s-3); }
}

@media (max-width: 960px) {
  .rail { display: none; }

  .topbar {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 16px;
    background: rgba(12, 12, 16, 0.92);
    backdrop-filter: blur(12px);
    border-bottom: 1px solid var(--line);
  }
  .topbar-right { display: flex; align-items: center; gap: 8px; }

  .tabbar {
    position: fixed;
    z-index: 30;
    bottom: 0; left: 0; right: 0;
    display: grid;
    /* Six items, six columns. It was `repeat(5, 1fr)` with six tabs in it, so the sixth wrapped
       onto a second row that sat on top of the page — visible in every mobile frame before this
       round. A long Spanish label ("Conexiones") is allowed to break rather than be clipped. */
    grid-template-columns: repeat(6, 1fr);
    gap: 2px;
    padding: 6px 6px calc(6px + env(safe-area-inset-bottom));
    background: rgba(10, 10, 14, 0.96);
    backdrop-filter: blur(12px);
    border-top: 1px solid var(--line);
  }
  .tab {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    min-height: 56px;
    padding: 7px 1px;
    line-height: 1.12;
    border-radius: var(--radius);
    color: var(--ink-3);
    font-size: var(--fs-xs);
    font-weight: 650;
    text-align: center;
  }
  /* One tab column at 390px is about 57px wide, and "Connections" / "Conexiones" does not fit on
     one line — the label carries a soft hyphen so it breaks as "Connec-tions" rather than being
     chopped mid-syllable. What was left was the raggedness: a two-line label made its tab taller,
     the row grew to the tallest, and the five one-line tabs centred themselves against it, so the
     six icons sat at three different heights. Reserving two lines for every label puts every icon
     back on one line, whether its own label wraps or not. --fs-xs is the floor for type in this
     app, so shrinking the words was not on the table. */
  .tab > span {
    display: block;
    min-height: calc(2 * 1.12em);
    overflow-wrap: break-word;
  }
  .tab .icon { color: var(--ink-3); }
  .tab.is-active { color: var(--purple-light); background: rgba(106, 95, 211, 0.16); }
  .tab.is-active .icon { color: var(--purple-light); }

  .outlet { padding: var(--s-5) var(--s-4) 104px; }
  .page-title { font-size: 27px; }
  .toasts { bottom: calc(72px + env(safe-area-inset-bottom)); left: 12px; right: 12px; }
  .toast { max-width: none; }
}

@media (max-width: 860px) {
  /* The table becomes one card per row; each cell prints its own column name. */
  .table-wrap { border: 0; background: transparent; overflow: visible; }
  /* `display: block` strips a table of its role, so the markup carries explicit ARIA roles; and the
     header row is *hidden*, not removed, because `display: none` takes the column names out of the
     accessibility tree and leaves every stacked card a run of unlabelled numbers. */
  .data-table thead {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
  .data-table, .data-table tbody { display: block; }
  .data-table tr {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 2px 12px;
    padding: 12px 14px;
    margin-bottom: 8px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
  }
  .data-table td {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
    padding: 3px 0;
    border: 0;
    text-align: left;
  }
  .data-table td.num { text-align: right; }
  /* The column name, as a real element rather than generated content: `content: attr(...)` is
     decoration, and this is the only label the stacked card has. It is `aria-hidden`, because the
     reader who hears it has already heard the column header. */
  .cell-label {
    display: block;
    font-size: var(--fs-xs);
    font-weight: 650;
    letter-spacing: 0.04em;
    color: var(--ink-3);
    flex: none;
  }
  .data-table td:nth-child(1),
  .data-table td:nth-child(2) { grid-column: 1 / -1; }
  .data-table td:nth-child(1) .cell-label,
  .data-table td:nth-child(2) .cell-label { display: none; }
  .data-table td:nth-child(2) { padding-bottom: 7px; border-bottom: 1px solid var(--line); margin-bottom: 4px; }
  .data-table .change-title { max-width: none; }
  .resp-cell { justify-content: flex-end; }

  /* Five stacked fields were 490px of filter above the first site card on a phone. Two across,
     with the search and the tail spanning, is half that and still every control. */
  .filter-bar { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: end; }
  .field--search { flex: none; max-width: none; grid-column: 1 / -1; }
  .filter-tail { grid-column: 1 / -1; margin-left: 0; justify-content: space-between; }
  /* The four drop-downs, folded away until asked for: below 860px they are built inside this
     <details> (see overview.js) and stacked they were 250px of filter above the first site card. */
  .filter-more { grid-column: 1 / -1; }
  .filter-more-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; padding-top: 10px; }
  .filter-more-summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    grid-column: 1 / -1;
    min-height: 44px;
    padding: 0 2px;
    font-size: var(--fs-base);
    font-weight: 650;
    color: var(--ink-2);
    cursor: pointer;
  }
  .filter-more-summary::-webkit-details-marker { display: none; }
  .filter-more[open] .filter-more-summary .icon { transform: rotate(180deg); }
  .filter-more-summary .icon { transform: rotate(90deg); transition: transform var(--t) var(--ease); }

  .form-grid { grid-template-columns: minmax(0, 1fr); }
  .report-row { grid-template-columns: minmax(0, 1fr); gap: 12px; }
  .report-actions { flex-direction: column; }
  .meta-bar { gap: 14px 20px; }
  .timeline-item { grid-template-columns: 74px 12px minmax(0, 1fr); gap: 8px; }
  .dev-row { flex-direction: column; align-items: flex-start; gap: 10px; }
  .block--danger .danger-row { flex-direction: column; align-items: flex-start; }
  .mail-stage { padding: 10px; }
  .mail-frame { height: 560px; }
  .wp-sync { margin-left: 0; }
  .chart-pair { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 460px) {
  .outlet { padding: 16px 12px 92px; }
  .stat-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* At 360px a two-word label wraps in the fallback font and not in Plus Jakarta Sans, so the whole
     page jumped when the webfont arrived — 0.081 of layout shift, all of it after first paint.
     Reserving two lines below 460px costs nothing (the labels are already two lines there in
     Spanish) and holds the page still. Round 2 rejected this at *every* width, and was right to. */
  .stat-label { min-height: 2.6em; }
  .stat-value { font-size: 27px; }
  /* The slim tile is two lines here, not one: at 390px the figure and its sub-line together are
     wider than half the screen, and `overflow: hidden` was cutting "Across every site" in half. */
  .stat--slim .stat-label { min-height: 0; }
  .stat--slim .stat-figure { flex-wrap: wrap; gap: 0 var(--s-2); }
  .stat--slim .stat-sub, .stat--slim .stat-sub--sev { flex-basis: 100%; }
  .stat--slim .stat-value { font-size: var(--fs-xl); }
  /* Same reservation on the slim tile, for the same reason — the label is one line in Plus Jakarta
     Sans and two in the fallback, and a tile that grows when the webfont lands is layout shift. */
  .stat--slim .stat-value { font-size: var(--fs-xl); }
  .page-actions, .page-actions-slot { width: 100%; }
  .page-actions .btn, .page-actions-slot .btn { flex: 1 1 auto; }
  .flag-row { grid-template-columns: 24px minmax(0, 1fr) 18px; row-gap: 5px; }
  .flag-age { grid-column: 2; }
  .flag-list--plain .flag-row { grid-template-columns: 24px minmax(0, 1fr); }
  .data-table tr { grid-template-columns: minmax(0, 1fr); }
  .logo-preview { width: 100%; }
  .all-clear { flex-direction: column; align-items: flex-start; gap: var(--s-4); }
}

/* ------------------------------------------------------ touch targets --
   WCAG 2.2 AA (2.5.8) asks 24px; the brief asks 44 on touch. `pointer: coarse` is the honest test —
   a finger, not a narrow window — so nothing grows on a laptop that happens to be 900px wide.
   Every value below is padding, so no layout moves when the query does not match. */

@media (pointer: coarse) {
  .btn { min-height: 44px; padding-top: 11px; padding-bottom: 11px; }
  .icon-btn { width: 44px; height: 44px; }
  .lang-btn { min-height: 38px; padding: 9px 16px; }
  .seg { min-height: 38px; padding: 9px 16px; }
  .field-input { min-height: 44px; }
  .switch { min-height: 44px; }
  .qs-item { min-height: 44px; }
  .note-preview { min-height: 44px; }
  .data-table .row--link td { padding-top: 10px; padding-bottom: 10px; }
}

/* --------------------------------------------------------- reduced motion */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  /* The cube is a still picture of the mark: no bounce (the rule above already collapses it), and
     no lean either — that one is a JS transform, so it needs saying here as well. */
  .cube-stage .cube-hop, .cube-stage .cube-spin, .cube-stage .cube-shadow,
  .cube-stage .cube-glow, .cube-face--front::after { animation: none !important; }
  .cube-tilt { transform: none !important; }
  .cube-stage { cursor: default; }
}

/* -------------------------------------------------- the report's text twin */

.plain-text {
  margin-top: var(--s-3);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-2);
}
.plain-text-summary {
  padding: 10px 14px;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-3);
  cursor: pointer;
  list-style: none;
}
.plain-text-summary::-webkit-details-marker { display: none; }
.plain-text-summary::before { content: '▸ '; color: var(--ink-4); }
.plain-text[open] .plain-text-summary { border-bottom: 1px solid var(--line); color: var(--ink-2); }
.plain-text[open] .plain-text-summary::before { content: '▾ '; }
.plain-text-body {
  margin: 0;
  padding: var(--s-4);
  max-height: 420px;
  overflow: auto;
  font-family: var(--mono);
  font-size: var(--fs-sm);
  line-height: 1.6;
  color: var(--ink-2);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.login-foot {
  margin-top: var(--s-4);
  padding-top: var(--s-3);
  border-top: 1px solid var(--line);
  font-size: var(--fs-sm);
  color: var(--ink-3);
}

/* ------------------------------------------------------ forced colours -- */
/* Windows High Contrast (and `forced-colors: active` anywhere else) throws every colour away and
   repaints with the reader's own palette. Everything this app uses a *shadow* for disappears, every
   tint flattens to one background, and the four status hues become one. So: borders where shadows
   carried structure, system colours for the things that must stay apart, and `forced-color-adjust:
   none` on the chart marks — the one place where the hue is the data and not decoration. Status is
   still shape + label + hue, so nothing is lost when the hue goes. */

@media (forced-colors: active) {
  .card, .stat, .toast, .dialog, .flag-row, .empty, .table-wrap, .qs-item, .conn-card {
    border: 1px solid CanvasText;
    box-shadow: none;
  }

  :focus-visible { outline: 3px solid Highlight; outline-offset: 2px; }
  ::selection { background: Highlight; color: HighlightText; }

  .btn--primary, .btn--danger { border: 1px solid ButtonText; forced-color-adjust: none; }
  .nav-link.is-active, .tab.is-active, .row--link:hover { background: Highlight; color: HighlightText; }
  .nav-link.is-active .icon, .tab.is-active .icon { color: HighlightText; }

  /* Charts keep their own colours: the series hue, the status hues and the failed-day hatch are the
     data. The hatch and the cell heights mean the strip still reads if a reader forces them anyway. */
  .viz-svg, .spark { forced-color-adjust: none; }
  .viz-gridline { stroke: CanvasText; opacity: 0.4; }
  .viz-tick { fill: CanvasText; }
  .viz-tip { border: 1px solid CanvasText; background: Canvas; color: CanvasText; }

  .mock-chip, .pill, .status, .sev, .delta { border: 1px solid CanvasText; }
  .skip-link { border: 2px solid CanvasText; }
}

/* ------------------------------------------------------------------ marks --
   The small square beside a site, a client or a flag: their logo, their site's
   favicon, or their initials. Most marks are drawn for a white page, so a plate
   goes under them in this dark UI — the same answer the client panel already
   gives a client logo in dark mode. It is decoration: `aria-hidden`, and the
   name is always beside it. */

.mark {
  position: relative;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: #ffffff;
  border: 1px solid var(--line);
  border-radius: 8px;
  width: var(--mark-size, 28px);
  height: var(--mark-size, 28px);
  box-shadow: 0 2px 8px -4px rgba(0, 0, 0, 0.8);
}
.mark--xs { --mark-size: 20px; border-radius: 6px; }
.mark--sm { --mark-size: 28px; }
.mark--rg { --mark-size: 32px; border-radius: 10px; }
.mark--tb { --mark-size: 36px; border-radius: 11px; }
.mark--md { --mark-size: 40px; border-radius: 12px; }
.mark--lg { --mark-size: 48px; border-radius: 14px; }
.mark--xl { --mark-size: 56px; border-radius: 16px; }
.mark-img { width: 100%; height: 100%; object-fit: contain; padding: 3px; }
/* A favicon already carries its own background and its own corner; it fills the tile instead of
   floating in the middle of a white plate at 60 % of the size it was drawn at. */
.mark--icon { background: transparent; border-color: rgba(255, 255, 255, 0.1); }
.mark--icon .mark-img { object-fit: cover; padding: 0; }

/* ── Monogram tiles ────────────────────────────────────────────────────────
   Round 4 §G3: 22px of grey letters on grey is not an identity, and four clients in four different
   businesses looked identical in a list. A monogram is *designed* now — a two-stop gradient of a
   hue derived from the name, an inner highlight, white initials in the display face, and a
   gradient angle that also comes from the name, so two clients sharing an initial still differ.

   The hue arrives as `--mark-h`, which ui.js sets per mark from a small hash into a ten-hue ring
   that deliberately skips the status bands: no monogram can land on the critical red, the warning
   amber or the ok green, so a tile can never be mistaken for a state. */
.mark--initials {
  background:
    linear-gradient(var(--mark-angle, 145deg),
      hsl(var(--mark-h, 258) 52% 58%),
      hsl(var(--mark-h, 258) 56% 33%));
  border-color: rgba(255, 255, 255, 0.14);
  color: #fff;
  font-family: var(--font-display);
  font-size: calc(var(--mark-size, 28px) * 0.4);
  font-weight: 700;
  letter-spacing: 0.01em;
  line-height: 1;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
}
/* Two passes of light over the gradient, so the tile is an object rather than a coloured square:
   a soft highlight falling from the top-left corner, and a rim. */
.mark--initials::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(85% 70% at 22% 12%, rgba(255, 255, 255, 0.35), transparent 62%);
  pointer-events: none;
}
.mark--initials::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.4),
    inset 0 0 0 1px rgba(255, 255, 255, 0.1),
    inset 0 -12px 20px -10px rgba(0, 0, 0, 0.6);
  pointer-events: none;
}
.mark--initials .mark-initials { position: relative; }

/* ── Wordmarks ─────────────────────────────────────────────────────────────
   A client's wide mark, where there is room for one (the client page's header). A mark drawn for
   white paper keeps a white plate under it here, because the hub is #0c0c10 and a near-black
   wordmark on it is no wordmark at all — the same plate a brand without a dark variant gets. */
.wordmark { display: inline-flex; align-items: center; flex: 0 0 auto; max-width: min(260px, 52vw); }
.wordmark-img { height: var(--wordmark-h, 34px); width: auto; max-width: 100%; object-fit: contain; }
.wordmark--plate {
  background: #ffffff;
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 7px 11px;
}

.site-cell--marked { flex-direction: row; align-items: center; gap: 11px; }
.site-cell-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }

.site-title-row .mark { margin-right: 2px; }

/* ------------------------------------------------------------ client cards --
   Clients was a four-row table with a whole empty page under it and the logo as the smallest thing
   on screen (round 4 §G3, §3). It is a grid of logo-led cards now, and still a grid at twenty. */

.client-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: var(--s-4);
}

.client-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  padding: var(--s-5);
  border-radius: var(--radius-lg);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--lift);
  overflow: hidden;
  transition: border-color var(--t-fast) ease, background var(--t-fast) ease,
              transform var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.client-card:hover {
  border-color: var(--line-2);
  background: var(--surface-2);
  color: var(--ink);
  transform: translateY(-2px);
  box-shadow: var(--lift-hover);
}
.client-card:active { transform: translateY(0); }
/* The card's own hue, from the same monogram hash, as a wash behind the mark.
   10 %, not the 16 % it was first drawn at: at 16 % the muted slug line measured 4.42 : 1 against
   the tinted surface underneath it, and this project promises 4.5. Measured, not eyeballed. */
.client-card::before {
  content: '';
  position: absolute;
  inset: 0 auto auto 0;
  width: 66%; height: 100%;
  background: radial-gradient(90% 120% at 0% 0%, hsl(var(--mark-h, 258) 55% 50% / 0.10), transparent 68%);
  pointer-events: none;
}

.client-card-head { position: relative; display: flex; align-items: center; gap: var(--s-3); min-width: 0; }
.client-card-text { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.client-card-name { font-size: var(--fs-xl); font-weight: 700; letter-spacing: -0.022em; line-height: 1.15; }
.client-card-slug { font-size: var(--fs-sm); color: var(--ink-3); font-family: var(--mono); }
.client-card-facts { display: flex; flex-direction: column; gap: 7px; }
.client-card-row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-3); font-size: var(--fs-sm); }
.client-card-row dt { color: var(--ink-3); flex: none; }
.client-card-row dd { color: var(--ink-2); text-align: right; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.client-card-tags { display: flex; gap: 6px; flex-wrap: wrap; margin-top: auto; padding-top: var(--s-1); }

/* ---------------------------------------------------- the confirm burst --
   Confirming a report is the one moment of satisfaction in the product, and it used to print a
   12.5px toast in a corner. The toast stays — it is what a screen reader hears — and this is what
   the eye gets. 420ms, once, and nothing at all under prefers-reduced-motion. */

.burst {
  position: fixed;
  z-index: 70;
  width: 140px; height: 140px;
  margin: -70px 0 0 -70px;
  pointer-events: none;
  color: var(--ok);
  animation: burst 420ms var(--ease) forwards;
}
@keyframes burst {
  from { opacity: 0.95; transform: scale(0.35); }
  60% { opacity: 0.6; }
  to { opacity: 0; transform: scale(1.3); }
}
@media (prefers-reduced-motion: reduce) { .burst { display: none; } }

/* ----------------------------------------------------------------- brands --
   The accent picker's second half: the colour, and the ink it will really be
   printed in once the 4.5:1 correction has run. */

.brand-ink-swatch {
  display: inline-block;
  width: 11px;
  height: 11px;
  border-radius: 3px;
  margin-right: 6px;
  vertical-align: -1px;
  border: 1px solid var(--line);
}
.brand-ink-note { color: var(--ink-3); }

/* A confirmed brand's "approved" report status: the hub's own interactive purple, which is neither
   a good/warning/critical status nor the neutral grey of a draft. */
.pill--accent {
  background: rgba(106, 95, 211, 0.16);
  color: var(--purple-light);
  border-color: rgba(106, 95, 211, 0.38);
}

/* The prose editor's action slot sits in the block head beside its title. */
.block-actions-slot { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
