/* ============================================================================
   sitecms admin — stylesheet
   ============================================================================
   Hand-written plain CSS. There is NO Tailwind and no CSS build step in this
   project (the only bundler is esbuild, and it only builds the TipTap editor).
   Design tokens are CSS custom properties — that is the substitute for a config
   file here.

   ⚠️ NOTHING outside the token layer may hardcode a colour. Every rule below
   references a var(); a literal hex in a component rule is a bug, because it
   cannot participate in the dark theme and it is invisible to the contrast
   checker.

   ── Look ─────────────────────────────────────────────────────────────────
   Deliberately GENERIC admin-dashboard aesthetic: dark navy sidebar, neutral
   grey canvas, white cards, ONE blue accent for everything interactive. This is
   not Patricia's brand — it should read as "software". The gold/serif brand
   treatment was removed in Phase 8; the website keeps it, the admin does not.

   Colour carries meaning in exactly one place: post status (draft / needs
   review / scheduled / published) and lead state. Nowhere else. Accent blue
   means "you can interact with this", never "this is important".

   ── Themes ───────────────────────────────────────────────────────────────
   Two COMPLETE token sets: `:root` is light, `[data-theme='dark']` is dark.
   The theme is applied to <html> by an inline script in views/partials/head.ejs
   BEFORE first paint, so there is no flash of the wrong theme. Stored
   preference (localStorage `sitecms-theme`) always wins; prefers-color-scheme is
   consulted only when nothing is stored. No database column, no server round
   trip.

   ⚠️ Contrast: BOTH themes are verified independently by
   `node scripts/check-contrast.js`, which parses the two token blocks below out
   of this file and exits non-zero on a WCAG AA failure in either. It reads the
   real values, so a token edit here cannot silently drift away from its check.
   Add a colour here and add its pair there.

   Fonts: Montserrat only, SELF-HOSTED from src/public/admin/fonts/ — no CDN, no
   build step, no runtime dependency on fonts.googleapis.com. See the @font-face
   block below.
   ========================================================================== */

/* ── self-hosted font ─────────────────────────────────────────────────────── */
/* Montserrat arrives as a single VARIABLE font covering the whole 100–900 axis,
   so one 37KB file replaces every static weight. The range is declared 100–900
   because this stylesheet uses 600/700 for headings, badges, buttons and
   <strong>; narrowing it would make the browser synthesise those instead.

   ⚠️ LICENCE: Montserrat is SIL Open Font License 1.1. The licence and the
   copyright / Reserved Font Name notices ship alongside the woff2 files in
   src/public/admin/fonts/OFL.txt. Do not delete it.
   Montaga's woff2 is still in that directory and still covered by the same
   OFL.txt, but the admin NO LONGER REFERENCES IT — a serif display face was the
   strongest remaining brand signal, and this panel is meant to look generic.
   Headings are Montserrat at heavier weights instead.

   font-display: swap — text paints immediately in the fallback face and swaps
   when the webfont lands. An admin panel must never block on a font. */

@font-face {
  font-family: 'Montserrat';
  src: url('/admin/static/fonts/montserrat-variable-latin.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* ── tokens: LIGHT (default) ─────────────────────────────────────────────── */

:root {
  color-scheme: light;

  /* --- surfaces --- */
  --bg-page: #f4f6f8;          /* neutral grey canvas */
  --bg-card: #ffffff;
  --bg-sunken: #eef1f5;        /* table headers, inactive tabs, input wells */
  --bg-hover: #f7f9fb;         /* row / control hover */
  --bg-sidebar: #0f1b2d;       /* dark navy, both themes */
  --bg-sidebar-hover: #1c2b45;
  --bg-overlay: rgba(8, 15, 26, 0.55);   /* modal backdrop — decorative */

  /* --- text --- */
  --text: #111827;
  --text-muted: #5a6472;
  --text-on-dark: #d6deea;
  --text-on-dark-dim: #93a1b5;
  --text-on-dark-hi: #ffffff;  /* hover/active text ON the navy sidebar */

  /* --- accent: the ONE interactive colour --- */
  --accent: #2563eb;           /* primary buttons, active nav, selected states */
  --accent-hover: #1d4ed8;
  --accent-fg: #ffffff;        /* text/icons ON an accent fill */
  --accent-soft: #e8effd;      /* tinted surface (callouts, dropzone hover) */
  --accent-border: #2563eb;
  /* The sidebar is navy in BOTH themes, so anything accent-coloured sitting on
     it needs its own light value — the light theme's accent is a dark blue and
     is unreadable there. */
  --accent-on-dark: #93c5fd;
  --link: #1d4ed8;
  --focus: #1d4ed8;

  /* --- lines --- */
  --border: #e3e8ef;           /* decorative separators only */
  --border-strong: #767f8d;    /* identifies a control (WCAG 1.4.11) */
  --border-sidebar: #24334c;

  /* --- feedback --- */
  --danger: #b00020;           /* danger fill; --danger-fg is text on it */
  --danger-fg: #ffffff;
  --danger-on-surface: #b00020; /* danger text/icon ON a card */
  --danger-bg: #fdecee;
  --danger-text: #7a0016;
  --success-bg: #e6f2ea;
  --success-text: #12502b;

  /* --- status: the ONLY semantic colour in the UI --- */
  --st-draft-bg: #e8eaee;
  --st-draft-fg: #374151;
  --st-draft-bd: #6d7684;
  /* NEEDS REVIEW = ONE YELLOW FAMILY. Every yellow surface here is hue 42°,
     S 100% — the dashboard tile's #FFB300 (hsl 42 100 50) varied only in
     LIGHTNESS, so each context can hold its own contrast without leaving the
     family. Do not substitute a different hue for any of them.
       L 50%  fill      tile, badge ground, status-dot fill
       L 30%  ring      status-dot ring, badge border  (light)
       L 45%  ring      badge border                   (dark)
       L 10%  ink/tint  badge text (light+dark), chip ground (dark)
       L 92%  tint      chip ground (light)
       L 75%  tint ink  chip text  (dark)
       L 18%  ink       chip text  (light)                                   */
  --st-review-bg: #ffb300;     /* hsl(42 100% 50%) — SATURATED ground */
  --st-review-fg: #332400;     /* hsl(42 100% 10%) — 8.40:1 on that ground */
  --st-review-bd: #996b00;     /* hsl(42 100% 30%) — badge border + dot ring */
  /* The stale-locale chip keeps the PALE treatment the badge gave up. It is one
     of three tiny chips in a table cell (filled / stale / empty); a saturated
     yellow there would shout louder than the state deserves and would not match
     its neighbours. Same hue, different lightness — see CLAUDE.md. */
  --loc-stale-bg: #fff3d6;     /* hsl(42 100% 92%) */
  --loc-stale-fg: #5c4000;     /* hsl(42 100% 18%) — 8.70:1 on the tint */
  --st-sched-bg: #dde9fe;
  --st-sched-fg: #14417e;
  --st-sched-bd: #2563eb;
  --st-pub-bg: #dcf0e3;
  --st-pub-fg: #12502b;
  --st-pub-bd: #1e7a44;

  /* --- dashboard tile fills --- */
  /* ⚠️ An INDEPENDENT PALETTE, chosen by the site owner. These deliberately do
     NOT match the status badge tokens (--st-*) any more, and the badges are
     unchanged — see CLAUDE.md. Do not try to reconcile the two.
     The same fills are used in BOTH themes, with white text throughout.
     ⚠️ Six of these do not reach 4.5:1 with white text. That was accepted
     deliberately on 2026-08-02 for an internal, single-user admin;
     scripts/check-contrast.js lists each one by name, pinned to its exact hex,
     so changing any fill re-breaks the build. */
  --tile-fg: #ffffff;
  --tile-fg-dim: #ffffff;
  --tile-icon-wash: rgba(255, 255, 255, 0.22);
  --tile-posts-bg: #f64a8a;
  --tile-draft-bg: #94a3b8;
  --tile-review-bg: #ffb300;
  --tile-sched-bg: #2196f3;
  --tile-pages-bg: #01796f;
  --tile-media-bg: #1a3e72;
  --tile-leads-bg: #f57c00;
  --tile-inbox-bg: #d32f2f;
  /* The review tile's dark text. In the token layer like every other colour, so
     the contrast checker measures the pair that is actually drawn. */
  --tile-review-fg: #111827;
  --tile-review-fg-dim: #1f2937;
  /* The Site Images "Custom" pill. Its own token: it used to borrow the
     published-tile fill, which no longer exists. */
  --slot-custom-bg: #01796f;
  /* Status-dot rings — a darker shade of each dot's OWN hue. The yellow dot
     needs one (a true #FFB300 is only 1.79:1 on a white row, so the RING is what
     satisfies 1.4.11); the other three get the same treatment purely so the four
     read as one component rather than one ringed dot beside three flat ones. */
  --dot-ring-draft: #4b515c;
  --dot-ring-review: #996b00;   /* hsl(42 100% 30%) */
  --dot-ring-sched: #173c8f;
  --dot-ring-pub: #12492a;
  --tile-neutral-bg: var(--tile-posts-bg);   /* fallback for an unclassed tile */

  /* --- type --- */
  --font-body: 'Montserrat', 'Segoe UI', system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif;

  /* --- metrics --- */
  --sidebar-w: 216px;
  --radius: 8px;
  --radius-sm: 5px;
  --shadow-card: 0 1px 2px rgba(16, 24, 40, 0.05);
  --shadow-modal: 0 16px 48px rgba(8, 15, 26, 0.28);
  --gap: 16px;
}

/* ── tokens: DARK ────────────────────────────────────────────────────────── */
/* A complete set, not an override of a few values. Dark is where AA fails
   quietly: a colour that passes on white almost never passes on #121d30, so
   every foreground here is its own value rather than a reused light one. */

[data-theme='dark'] {
  color-scheme: dark;

  --bg-page: #0b1220;
  --bg-card: #121d30;
  --bg-sunken: #17233a;
  --bg-hover: #1a2740;
  --bg-sidebar: #0a101c;
  --bg-sidebar-hover: #17233a;
  --bg-overlay: rgba(2, 6, 12, 0.7);

  --text: #e6ebf2;
  --text-muted: #9dabbd;
  --text-on-dark: #d6deea;
  --text-on-dark-dim: #93a1b5;
  --text-on-dark-hi: #ffffff;

  /* The accent inverts: a light blue fill with dark text, because white on a
     mid blue does not reach 4.5:1 and a dark blue fill disappears into the
     card. */
  --accent: #60a5fa;
  --accent-hover: #93c5fd;
  --accent-fg: #0a101c;
  --accent-soft: #17274a;
  --accent-border: #60a5fa;
  --accent-on-dark: #93c5fd;
  --link: #8ec0ff;
  --focus: #8ec0ff;

  --border: #22304a;
  --border-strong: #6b7b93;
  --border-sidebar: #1d2942;

  --danger: #dc2626;
  --danger-fg: #ffffff;
  --danger-on-surface: #fca5a5;
  --danger-bg: #2b1216;
  --danger-text: #fca5a5;
  --success-bg: #10261b;
  --success-text: #7ee0a6;

  --st-draft-bg: #232f44;
  --st-draft-fg: #c6d0de;
  --st-draft-bd: #6b7a90;
  /* The badge is the SAME saturated yellow in both themes — a #FFB300 pill reads
     correctly on a dark card, so there is nothing to invert. */
  --st-review-bg: #ffb300;     /* hsl(42 100% 50%) */
  --st-review-fg: #332400;     /* hsl(42 100% 10%) — 8.40:1 */
  --st-review-bd: #e6a100;     /* hsl(42 100% 45%) — badge border on dark */
  --loc-stale-bg: #332400;     /* hsl(42 100% 10%) */
  --loc-stale-fg: #ffd980;     /* hsl(42 100% 75%) — 11.12:1 */
  --st-sched-bg: #14294a;
  --st-sched-fg: #a9cbff;
  --st-sched-bd: #4b8bf5;
  --st-pub-bg: #10291d;
  --st-pub-fg: #7ee0a6;
  --st-pub-bd: #2f9e63;

  /* The tile palette is IDENTICAL in dark — the owner picked one set of fills
     for both themes, so a tile reads the same either way. Repeated in full
     because every colour token must exist in both blocks (the contrast script
     fails on a token that is missing here). */
  --tile-fg: #ffffff;
  --tile-fg-dim: #ffffff;
  --tile-icon-wash: rgba(255, 255, 255, 0.22);
  --tile-posts-bg: #f64a8a;
  --tile-draft-bg: #94a3b8;
  --tile-review-bg: #ffb300;
  --tile-sched-bg: #2196f3;
  --tile-pages-bg: #01796f;
  --tile-media-bg: #1a3e72;
  --tile-leads-bg: #f57c00;
  --tile-inbox-bg: #d32f2f;
  /* The review tile's dark text. In the token layer like every other colour, so
     the contrast checker measures the pair that is actually drawn. */
  --tile-review-fg: #111827;
  --tile-review-fg-dim: #1f2937;
  /* The Site Images "Custom" pill. Its own token: it used to borrow the
     published-tile fill, which no longer exists. */
  --slot-custom-bg: #01796f;
  /* Status-dot rings — a darker shade of each dot's OWN hue. The yellow dot
     needs one (a true #FFB300 is only 1.79:1 on a white row, so the RING is what
     satisfies 1.4.11); the other three get the same treatment purely so the four
     read as one component rather than one ringed dot beside three flat ones. */
  --dot-ring-draft: #4b515c;
  --dot-ring-review: #996b00;   /* hsl(42 100% 30%) */
  --dot-ring-sched: #173c8f;
  --dot-ring-pub: #12492a;
  --tile-neutral-bg: var(--tile-posts-bg);

  --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-modal: 0 16px 48px rgba(0, 0, 0, 0.6);
}

/* ── base ─────────────────────────────────────────────────────────────── */

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

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: 14px;
  line-height: 1.55;
  color: var(--text);
  background: var(--bg-page);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 {
  font-family: var(--font-body);
  font-weight: 600;
  color: var(--text);
  margin: 0 0 0.4em;
  letter-spacing: 0.01em;
}
h1 { font-size: 23px; letter-spacing: -0.01em; }
h2 { font-size: 17px; }
h3 { font-size: 15px; }

a { color: var(--link); }
a:hover { text-decoration: none; }

code {
  font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
  font-size: 0.92em;
}

/* Visible focus everywhere. Never remove without a replacement. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: 2px;
}

.muted { color: var(--text-muted); }

/* ── layout shell ─────────────────────────────────────────────────────── */
/* The nav markup is unchanged (<nav class="nav">…</nav>) — it is repositioned
   into a sidebar purely in CSS, so no template restructuring was needed. */

.nav {
  position: fixed;
  top: 0; left: 0; bottom: 0;
  width: var(--sidebar-w);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
  padding: 20px 0 12px;
  background: var(--bg-sidebar);
  color: var(--text-on-dark);
  overflow-y: auto;
  z-index: 20;
}

/* The brand is a link to the dashboard, so it needs its own rules — the
   generic `.nav a` treatment (body font, left indicator bar) would flatten it
   into just another menu item. */
.nav a.nav-brand {
  display: block;
  font-family: var(--font-body);
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--accent-on-dark);
  text-decoration: none;
  padding: 0 18px 16px;
  margin: 0 0 8px;
  border-left: 0;
  border-bottom: 1px solid var(--border-sidebar);
  border-radius: 0;
}
.nav a.nav-brand:hover { color: var(--text-on-dark-hi); background: transparent; }

.nav a {
  color: var(--text-on-dark);
  text-decoration: none;
  padding: 8px 18px;
  font-size: 13.5px;
  border-left: 3px solid transparent;
  transition: background-color 0.12s ease, border-color 0.12s ease;
}
.nav a:hover { color: var(--text-on-dark-hi); background: var(--bg-sidebar-hover); }

/* Current page. Driven by aria-current in the nav partial, so the cue is
   exposed to assistive tech and never carried by colour alone. */
.nav a[aria-current='page'] {
  background: var(--accent);
  color: var(--accent-fg);
  border-left-color: var(--accent-hover);
  font-weight: 600;
}

.nav-logout {
  margin: auto 18px 0;
  background: transparent;
  border: 1px solid var(--text-on-dark-dim);
  color: var(--text-on-dark);
  padding: 7px 12px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font: inherit;
  font-size: 13px;
}
.nav-logout:hover { background: var(--bg-sidebar-hover); color: var(--text-on-dark-hi); }

.container {
  max-width: 1120px;
  margin: 0 0 0 var(--sidebar-w);
  padding: 28px 32px 8px;
}

/* ── page header ──────────────────────────────────────────────────────── */

.page-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--gap);
  flex-wrap: wrap;
  padding-bottom: 14px;
  margin-bottom: 20px;
  border-bottom: 1px solid var(--border);
}
.page-header h1 { margin: 0; }
.page-header > div { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; }

/* Pages whose h1 is not inside a .page-header wrapper get the same rule. */
.container > h1 {
  padding-bottom: 14px;
  margin-bottom: 20px;
  border-bottom: 1px solid var(--border);
}

/* ── cards ────────────────────────────────────────────────────────────── */

.card,
.data-table,
.filters,
.lead-filters,
.lang-panel,
.account-form,
.slot-group {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
}

.filters,
.lead-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  align-items: flex-end;
  padding: 14px 16px;
  margin: 0 0 18px;
}
.filters label,
.lead-filters label {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin: 0;
  font-size: 12px;
  color: var(--text-muted);
}

/* ── forms ────────────────────────────────────────────────────────────── */

input[type='text'],
input[type='email'],
input[type='password'],
input[type='search'],
input[type='number'],
input[type='datetime-local'],
select,
textarea {
  font: inherit;
  font-size: 13.5px;
  color: var(--text);
  background: var(--bg-card);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 7px 9px;
  width: 100%;
}
textarea { resize: vertical; }

input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
  border-color: var(--focus);
}

label { display: block; margin-bottom: 12px; font-size: 13px; }
label > input, label > select, label > textarea { margin-top: 5px; }

.field-row {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  padding: 16px;
  margin: 0 0 18px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
}
.field-row > label { flex: 1 1 220px; margin-bottom: 0; }

.field-hint { display: block; margin-top: 4px; font-size: 11.5px; color: var(--text-muted); }

/* ── buttons ──────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  padding: 9px 14px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--bg-card);
  color: var(--text);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
}
.btn:hover { background: var(--bg-sunken); }

.btn-primary {
  background: var(--accent);
  border-color: var(--accent-border);
  color: var(--accent-fg);
}
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }

.btn-delete,
.btn-danger {
  background: var(--danger);
  border-color: var(--danger);
  color: var(--danger-fg);
}
.btn-delete:hover, .btn-danger:hover { background: var(--danger-text); }

.btn-sm { padding: 6px 10px; font-size: 12px; }
.btn:disabled, .btn[disabled] { opacity: 0.5; cursor: not-allowed; }

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--bg-card);
  color: var(--text);
  cursor: pointer;
}
.icon-btn:hover { background: var(--bg-sunken); }
.icon-btn svg { width: 15px; height: 15px; display: block; pointer-events: none; }
.icon-btn-danger { color: var(--danger-on-surface); border-color: var(--border-strong); }
.icon-btn-danger:hover { background: var(--danger-bg); color: var(--danger-text); border-color: var(--danger); }

.row-actions { display: flex; gap: 6px; align-items: center; }

/* ── tables ───────────────────────────────────────────────────────────── */

.data-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  overflow: hidden;
  font-size: 13.5px;
  margin: 0 0 18px;
}
.data-table thead th {
  background: var(--bg-sunken);
  color: var(--text-muted);
  text-align: left;
  font-weight: 600;
  font-size: 11.5px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.data-table td {
  padding: 7px 12px;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
.data-table tbody tr:last-child td { border-bottom: 0; }
.data-table tbody tr:hover { background: var(--bg-hover); }
.data-table .num, .data-table .date { font-variant-numeric: tabular-nums; }

.sort-btn {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-size: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
  cursor: pointer;
}
.sort-btn:hover { text-decoration: underline; }

/* ── status badges ────────────────────────────────────────────────────── */
/* Four states, distinguished by TEXT LABEL + a distinct leading SHAPE + border,
   so none depends on colour alone (WCAG 1.4.1). The shape is decorative — the
   label already names the state.

   The shapes are DRAWN IN CSS (border-radius / border / background gradients on
   an empty ::before box), not typed as characters. Font glyphs were the earlier
   approach and they had a real problem: the self-hosted Montserrat latin subset
   covers U+0000–00FF, so ○ ◑ ◷ ● ✓ all fell through to whatever the operating
   system happened to ship — different metrics, different weights, different
   baselines on Windows / macOS / Linux, and nothing at all in a locked-down
   environment. A drawn box renders identically everywhere and depends on no
   font. It also removes the whole combining-mark hazard: there are no
   characters left here to pick badly.
   `currentColor` ties every shape to the badge's own text colour, so each one
   inherits a fill that has already been contrast-checked against its pill. */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11.5px;
  font-weight: 600;
  line-height: 1;
  padding: 4px 9px 4px 7px;
  border-radius: 999px;
  border: 1px solid transparent;
  white-space: nowrap;
}
/* The shared shape box. Every state below only changes its FILL. */
.badge::before {
  content: '';
  display: inline-block;
  flex: none;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  border: 1.5px solid currentColor;
  background: transparent;
}

.badge-draft {
  background: var(--st-draft-bg);
  color: var(--st-draft-fg);
  border-color: var(--st-draft-bd);
}
/* hollow ring — nothing committed (inherits the empty default) */

.badge-needs_review {
  background: var(--st-review-bg);
  color: var(--st-review-fg);
  border-color: var(--st-review-bd);
}
/* half filled — awaiting sign-off */
.badge-needs_review::before {
  background: linear-gradient(90deg, currentColor 0 50%, transparent 50% 100%);
}

.badge-scheduled {
  background: var(--st-sched-bg);
  color: var(--st-sched-fg);
  border-color: var(--st-sched-bd);
}
/* clock face with a wedge — waiting on time */
.badge-scheduled::before {
  background: conic-gradient(currentColor 0 90deg, transparent 90deg 360deg);
}

.badge-published {
  background: var(--st-pub-bg);
  color: var(--st-pub-fg);
  border-color: var(--st-pub-bd);
}
/* solid — live */
.badge-published::before { background: currentColor; }

/* --- API key status --- */
/* Separate from the post-state badges above. They previously shared
   badge-draft / badge-published, so a revoked key rendered with the "draft"
   shape — a post state that means nothing for a credential. A key is a
   credential: it either works or it does not, and the distinction worth
   drawing is "usable" vs "deliberately killed". */

.badge-active {
  background: var(--st-pub-bg);
  color: var(--st-pub-fg);
  border-color: var(--st-pub-bd);
}
/* check mark — usable. Two borders of a rectangle, rotated: the classic
   CSS tick, no font and no SVG. */
.badge-active::before {
  width: 5px;
  height: 9px;
  border-radius: 0;
  border-width: 0 1.5px 1.5px 0;
  transform: rotate(40deg) translate(0, -1px);
  background: transparent;
}

.badge-revoked {
  background: var(--danger);
  color: var(--danger-fg);
  border-color: var(--danger);
}
/* barred circle — deliberately killed */
.badge-revoked::before {
  background: linear-gradient(
    45deg,
    transparent calc(50% - 0.75px),
    currentColor calc(50% - 0.75px) calc(50% + 0.75px),
    transparent calc(50% + 0.75px)
  );
}

/* ── locale switcher ──────────────────────────────────────────────────── */
/* Which locale is being edited, and which locales already hold copy, both need
   to be readable at a glance. `has-content` is toggled by the editor's own
   script from data it already loaded — no extra requests, no new endpoints. */

.lang-tabs {
  display: flex;
  gap: 4px;
  margin: 0 0 -1px;
  padding: 0;
  position: relative;
  z-index: 1;
}

.lang-tab {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.04em;
  padding: 9px 16px;
  cursor: pointer;
  color: var(--text-muted);
  background: var(--bg-sunken);
  border: 1px solid var(--border);
  border-bottom-color: transparent;
  border-radius: var(--radius) var(--radius) 0 0;
}
.lang-tab:hover { color: var(--text); background: var(--bg-hover); }

.lang-tab.active {
  background: var(--accent);
  color: var(--accent-fg);
  border-color: var(--accent-border);
  border-bottom-color: var(--accent);
}

/* Content indicator: filled dot = this locale has copy, hollow ring = empty.
   CSS-drawn like the badges — no font glyph. The tab's title attribute carries
   the same fact as words for screen readers. */
.lang-tab::after {
  content: '';
  display: inline-block;
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: 1.5px solid var(--border-strong);
  background: transparent;
}
.lang-tab.has-content::after {
  border-color: var(--st-pub-bd);
  background: var(--st-pub-bd);
}
.lang-tab.active::after { border-color: var(--accent-fg); }
.lang-tab.active.has-content::after { background: var(--accent-fg); }

.lang-panel {
  padding: 20px;
  margin: 0 0 18px;
  border-radius: 0 var(--radius) var(--radius) var(--radius);
}

/* ── editor ───────────────────────────────────────────────────────────── */

.status-controls { display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap; }
.status-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin: 0;
  font-size: 12px;
  color: var(--text-muted);
}
.status-field select, .status-field input { min-width: 150px; }

.meta-counter {
  display: block;
  margin-top: 4px;
  font-size: 11.5px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.meta-counter.over { color: var(--danger); font-weight: 700; }

.readtime-input { max-width: 110px; }

.editor-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  padding: 7px 8px;
  background: var(--bg-sunken);
  border: 1px solid var(--border-strong);
  border-bottom: 0;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
.tb-group { display: flex; gap: 2px; }
.tb-group + .tb-group {
  margin-left: 6px;
  padding-left: 8px;
  border-left: 1px solid var(--border);
}
.editor-toolbar button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 30px;
  height: 28px;
  padding: 0 6px;
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  background: var(--bg-card);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 3px;
  cursor: pointer;
}
.editor-toolbar button:hover { background: var(--bg-sunken); }
.editor-toolbar button.active {
  background: var(--accent);
  border-color: var(--accent-border);
  color: var(--accent-fg);
}
.editor-toolbar svg { width: 15px; height: 15px; }

.tiptap-editor, .body-fallback {
  min-height: 320px;
  padding: 14px 16px;
  background: var(--bg-card);
  border: 1px solid var(--border-strong);
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  font-size: 14.5px;
  line-height: 1.65;
}
.tiptap-editor:focus-within { outline: 2px solid var(--focus); outline-offset: -1px; }
.tiptap-editor img { max-width: 100%; height: auto; }
.tiptap-editor a { color: var(--link); }
.tiptap-editor h2 { font-size: 19px; margin-top: 1em; }
.tiptap-editor h3 { font-size: 16px; margin-top: 1em; }
.body-fallback {
  width: 100%;
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 13px;
}

.editor-error,
.form-message.error {
  padding: 11px 14px;
  margin: 0 0 18px;
  background: var(--danger-bg);
  border: 1px solid var(--danger);
  border-left-width: 4px;
  border-radius: var(--radius-sm);
  color: var(--danger-text);
  font-size: 13px;
}
.form-message {
  padding: 11px 14px;
  margin: 0 0 18px;
  background: var(--success-bg);
  border: 1px solid var(--success-text);
  border-left-width: 4px;
  border-radius: var(--radius-sm);
  color: var(--success-text);
  font-size: 13px;
}

.publish-toggle { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; }

.featured-upload { display: inline-flex; align-items: center; gap: 8px; margin-top: 6px; }
.featured-upload-status, .img-upload-status { font-size: 12px; color: var(--text-muted); }

.upload-status-list { margin: 0 0 16px; padding: 0; list-style: none; font-size: 13px; }
.upload-status-list li { padding: 2px 0; }
.upload-status-list .ok { color: var(--success-text); }
.upload-status-list .err { color: var(--danger); }

/* ── media grid ───────────────────────────────────────────────────────── */

.media-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: var(--gap);
  margin: 0 0 20px;
}

.media-card {
  display: flex;
  flex-direction: column;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  overflow: hidden;
}
.media-card:hover { border-color: var(--border-strong); }

/* Uniform thumbnails regardless of the source aspect ratio. */
.media-card img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  background: var(--bg-sunken);
  border-bottom: 1px solid var(--border);
}

.media-card-meta { padding: 9px 10px 0; font-size: 12px; color: var(--text-muted); }
.media-card-meta .media-name {
  font-weight: 600;
  color: var(--text);
  word-break: break-all;
  line-height: 1.35;
}
.media-card-meta .media-detail { color: var(--text-muted); }
.media-card-actions {
  display: flex;
  gap: 6px;
  align-items: center;
  padding: 9px 10px 10px;
  margin-top: auto;
}

/* Selected state: accent ring plus a check mark, so it is not colour-only.
   The tick is CSS-drawn (two borders of a rotated box) for the same reason the
   badge shapes are — no dependency on a font that may not carry U+2713. */
.media-card.selected { border-color: var(--accent-border); box-shadow: 0 0 0 2px var(--accent); }
.media-card.selected .media-card-meta .media-name::after {
  content: '';
  display: inline-block;
  width: 5px;
  height: 9px;
  margin: 0 0 1px 6px;
  vertical-align: middle;
  border: solid var(--success-text);
  border-width: 0 2px 2px 0;
  transform: rotate(40deg);
}

.dropzone {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px 20px;
  margin: 0 0 20px;
  text-align: center;
  color: var(--text-muted);
  background: var(--bg-card);
  border: 2px dashed var(--border-strong);
  border-radius: var(--radius);
  font-size: 13.5px;
}
.dropzone.dragover { border-color: var(--accent-border); background: var(--accent-soft); color: var(--text); }

/* ── site image slots ─────────────────────────────────────────────────── */
/* ⚠️ The thumbnail is a CONTAINER, not the image. `.slot-thumb` used to carry
   `aspect-ratio` + `object-fit` while sitting on the wrapping <div>, so the
   <img> inside it inherited no constraint at all: a 3000px-tall source photo
   rendered at 3000px and ran the length of the page, and a slot whose file was
   missing collapsed to a broken-image icon sized to its alt text. The box is
   fixed here and the image is told to fill it. */

/* A neutral explanatory callout — not a warning, so it uses the accent tint
   rather than the danger family. */
.callout {
  padding: 12px 16px;
  margin: 0 0 18px;
  background: var(--accent-soft);
  border: 1px solid var(--accent-border);
  border-left-width: 4px;
  border-radius: var(--radius-sm);
  font-size: 13px;
}
.callout p { margin: 0 0 6px; }
.callout p:last-child { margin: 6px 0 0; font-size: 12px; }
.callout ul { margin: 0; padding-left: 18px; }
.callout li { margin: 3px 0; }

/* Jump nav — the grid runs to ~55 cards once neighborhoods and developments are
   included, which is too long to scan. Plain anchors, no scroll hijacking. */
.slot-jump {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 18px;
}
.slot-jump a {
  padding: 6px 11px;
  font-size: 12.5px;
  font-weight: 600;
  text-decoration: none;
  color: var(--text);
  background: var(--bg-card);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
}
.slot-jump a:hover { background: var(--bg-sunken); }

.slot-group { padding: 16px; margin: 0 0 18px; scroll-margin-top: 16px; }
.slot-group h2 { display: flex; align-items: center; gap: 8px; }
.slot-group-count {
  font-size: 11.5px;
  font-weight: 700;
  padding: 2px 8px;
  border-radius: 999px;
  color: var(--text-muted);
  background: var(--bg-sunken);
  border: 1px solid var(--border);
}
.slot-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(208px, 1fr));
  gap: var(--gap);
  align-items: start;
}
.slot-card {
  display: flex;
  flex-direction: column;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.slot-thumb {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
  max-height: 190px;          /* belt and braces if aspect-ratio is unsupported */
  overflow: hidden;
  background: var(--bg-sunken);
  border-bottom: 1px solid var(--border);
}
/* No image may exceed its cell, whatever its natural size. It is absolutely
   positioned so it LAYERS OVER the placeholder below rather than replacing it:
   a slot that has not loaded (or never will) shows "NO IMAGE" without waiting
   on an error event, and a slot that loads covers it completely. */
.slot-thumb img {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.slot-badge {
  position: absolute;
  top: 8px;
  right: 8px;
  display: inline-block;
  padding: 2px 7px;
  border-radius: 999px;
  font-size: 10.5px;
  font-weight: 700;
  background: var(--slot-custom-bg);
  color: var(--tile-fg);
}
.slot-badge.default {
  background: var(--st-draft-bg);
  color: var(--st-draft-fg);
  border: 1px solid var(--st-draft-bd);
}

/* "NO IMAGE" placeholder — drawn entirely in CSS, so there is no asset to ship
   and it themes itself.
   It is in the DOM from the start, underneath the image, rather than being
   swapped in when one fails: an error-event-only approach leaves the cell blank
   for a lazy image that has not been scheduled yet, and for a request that hangs
   rather than failing.
   ⚠️ It is HIDDEN the moment its image loads (`.is-loaded`, set by the template's
   load handler). "Covered by the image" is not good enough — a PNG with an alpha
   channel is not opaque, so the dashed well and the word NO IMAGE showed THROUGH
   the transparent trust badges. Layer for the pending case, hide for the loaded
   one. */
.slot-thumb.is-loaded .slot-empty { display: none; }

.slot-empty {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 10px;
  text-align: center;
  background: var(--bg-sunken);
  border: 2px dashed var(--border-strong);
  color: var(--text-muted);
}
.slot-empty-text {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.12em;
}
.slot-empty-hint { font-size: 10.5px; letter-spacing: 0.02em; }
/* A crossed-out frame, drawn with borders and a gradient — no font glyph. */
.slot-empty::before {
  content: '';
  width: 34px;
  height: 26px;
  border: 2px solid currentColor;
  border-radius: 3px;
  background: linear-gradient(
    -45deg,
    transparent calc(50% - 1px),
    currentColor calc(50% - 1px) calc(50% + 1px),
    transparent calc(50% + 1px)
  );
  opacity: 0.75;
}

.slot-meta { padding: 9px 10px 4px; }
.slot-label {
  font-size: 13.5px;
  font-weight: 600;
  line-height: 1.3;
  /* Two lines' worth whether the label wraps or not, so every card in a row is
     the same height and the grid stays aligned. */
  min-height: calc(1.3em * 2);
}
.slot-key { font-size: 11px; color: var(--text-muted); word-break: break-all; }
.slot-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 8px 10px 10px; margin-top: auto; }
.slot-actions .act-reset:disabled { opacity: 0.4; cursor: default; }
.slot-status { font-size: 12px; color: var(--text-muted); }

/* ── leads ────────────────────────────────────────────────────────────── */

.lead-counts { font-size: 13px; color: var(--text-muted); margin: 0 0 12px; }
.lead-message {
  font-size: 12px;
  color: var(--text-muted);
  margin-top: 3px;
  max-width: 340px;
  white-space: pre-wrap;
}
.lead-page, .lead-utm, .lead-context {
  font-size: 11px;
  color: var(--text-muted);
  margin-top: 2px;
  word-break: break-all;
}
.notes-input { width: 200px; font-size: 12px; padding: 5px 7px; }
.status-select { font-size: 12px; padding: 5px 7px; width: auto; }

/* ── api keys ─────────────────────────────────────────────────────────── */

.new-key {
  padding: 14px 16px;
  margin: 0 0 18px;
  background: var(--accent-soft);
  border: 1px solid var(--accent-border);
  border-left-width: 4px;
  border-radius: var(--radius-sm);
}
.new-key p { margin: 0 0 8px; font-size: 13px; }
.new-key-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.new-key-row code {
  font-size: 13px;
  background: var(--bg-card);
  border: 1px solid var(--accent-border);
  border-radius: 3px;
  padding: 7px 9px;
  word-break: break-all;
}

/* ── account / login ──────────────────────────────────────────────────── */

.account-form { padding: 20px; max-width: 460px; }

.login-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  margin: 0;
  background: var(--bg-sidebar);
}
.login-card {
  width: 340px;
  padding: 30px 28px;
  background: var(--bg-card);
  border-radius: var(--radius);
  box-shadow: var(--shadow-modal);
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.login-card h1 { margin: 0 0 4px; font-size: 22px; text-align: center; }
.login-card label { display: flex; flex-direction: column; gap: 5px; margin: 0; font-size: 13px; }

.login-body .site-footer { margin: 22px 0 0; border-top: 0; }
.login-body .site-footer p { color: var(--text-on-dark-dim); }
.login-body .site-footer a { color: var(--accent-on-dark); }

.error { color: var(--danger); font-size: 13px; }

/* ── footer ───────────────────────────────────────────────────────────── */
/* On every admin page. No "Made in …" line — only the Netplus1 credit, matching
   the treatment on patriciaalvares.com. */

.site-footer {
  margin: 36px 0 0 var(--sidebar-w);
  padding: 16px 32px 22px;
  border-top: 1px solid var(--border);
  text-align: center;
}
.site-footer p { margin: 0; font-size: 12px; color: var(--text-muted); }
.site-footer a { color: var(--link); }

/* ── responsive: tablet ───────────────────────────────────────────────── */
/* At tablet width the sidebar becomes a horizontal bar. Phone is explicitly not
   a priority for the admin; below ~700px it stays usable but is not tuned. */

@media (max-width: 900px) {
  :root { --sidebar-w: 0px; }

  .nav {
    position: static;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    width: auto;
    gap: 0;
    padding: 10px 16px;
  }
  .nav a.nav-brand {
    padding: 0 16px 0 0;
    margin: 0 8px 0 0;
    border-bottom: 0;
    border-right: 1px solid var(--border-sidebar);
    font-size: 17px;
  }
  .nav a {
    padding: 7px 11px;
    border-left: 0;
    border-bottom: 3px solid transparent;
    border-radius: var(--radius-sm);
  }
  .nav a[aria-current='page'] {
    border-left-color: transparent;
    border-bottom-color: var(--accent-hover);
  }
  /* In the horizontal bar the two controls sit together at the right end. */
  .theme-toggle { margin: 0 0 0 auto; padding: 6px 10px; }
  .nav-logout { margin: 0 0 0 8px; }

  .container { margin-left: 0; padding: 22px 20px 8px; }
  .site-footer { margin-left: 0; padding: 16px 20px 22px; }

  .field-row > label { flex-basis: 100%; }
  .media-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }

  /* Wide tables scroll horizontally rather than crushing every column. */
  .table-scroll { overflow-x: auto; }
  .table-scroll .data-table { min-width: 720px; }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; scroll-behavior: auto !important; }
}

/* ── per-locale completeness (post list) ──────────────────────────────── */
/* Three small chips per row. Filled / stale / empty differ by fill, border AND
   shape, and each carries a title attribute stating the state in words — so the
   information is never colour-only. Same rule as .badge above: the shapes are
   CSS-drawn and decorative, the tooltip carries the meaning. */

.locale-cell { white-space: nowrap; }

.loc {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1;
  padding: 3px 6px;
  margin-right: 3px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
}
/* Same drawn-shape approach as .badge: an empty box filled with CSS. */
.loc::after {
  content: '';
  display: inline-block;
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  border: 1.25px solid currentColor;
  background: transparent;
}

.loc-filled {
  background: var(--st-pub-bg);
  color: var(--st-pub-fg);
  border-color: var(--st-pub-bd);
}
.loc-filled::after { background: currentColor; }               /* solid — present */

.loc-stale {
  background: var(--loc-stale-bg);
  color: var(--loc-stale-fg);
  border-color: var(--st-review-bd);
}
/* half filled — behind a sibling */
.loc-stale::after {
  background: linear-gradient(90deg, currentColor 0 50%, transparent 50% 100%);
}

.loc-empty {
  background: var(--bg-sunken);
  color: var(--text-muted);
  border-color: var(--border-strong);
  border-style: dashed;
}
/* flat bar — nothing here */
.loc-empty::after {
  width: 7px;
  height: 1.5px;
  border: 0;
  border-radius: 0;
  background: currentColor;
}

/* "N posts need translation work" banner, and the editor's drift warning.
   These are WARNINGS about the state of the content, not a status label, so they
   use the danger/notice family rather than the amber "needs review" status
   tokens they used to borrow. Borrowing a status colour for a warning made the
   banner look like a fifth post state. */
.needs-translation,
.drift-notice {
  padding: 10px 14px;
  margin: 0 0 16px;
  background: var(--danger-bg);
  border: 1px solid var(--danger);
  border-left-width: 4px;
  border-radius: var(--radius-sm);
  color: var(--danger-text);
  font-size: 13px;
}
.needs-translation a,
.drift-notice a { color: var(--danger-text); font-weight: 600; }

/* ── utilities ────────────────────────────────────────────────────────── */

/* Visible to screen readers, not to sighted users. Used for labels on
   in-table controls where a visible label would be noise. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── post editor: slug + featured image ───────────────────────────────── */

.slug-row { display: flex; align-items: center; gap: 8px; }
.slug-row input { flex: 1 1 auto; }
/* A frozen slug reads as data, not as an editable field. */
.slug-row input:read-only { background: var(--bg-sunken); color: var(--text-muted); }

.featured-picker { display: inline-flex; align-items: center; gap: 8px; margin-top: 6px; flex-wrap: wrap; }
.featured-preview {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 8px;
  font-size: 12px;
}
.featured-preview img {
  width: 84px;
  height: 63px;
  object-fit: cover;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-sunken);
}
.featured-name { font-weight: 600; word-break: break-all; }

/* ── media picker modal ───────────────────────────────────────────────── */
/* A native <dialog>: Escape-to-close, an inert background and focus
   containment come from the platform rather than from a hand-rolled trap. */

.media-modal {
  width: min(880px, 92vw);
  max-height: 84vh;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--bg-card);
  color: var(--text);
  overflow: hidden;
}
.media-modal::backdrop { background: var(--bg-overlay); }
.media-modal-head,
.media-modal-controls,
.media-modal-foot {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 16px;
}
.media-modal-head { justify-content: space-between; border-bottom: 1px solid var(--border); }
.media-modal-head h2 { margin: 0; font-size: 18px; }
.media-modal-controls { flex-wrap: wrap; border-bottom: 1px solid var(--border); }
.media-modal-controls input[type='search'] { flex: 1 1 220px; }
.media-modal-foot { justify-content: center; border-top: 1px solid var(--border); }

.media-modal-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 12px;
  padding: 16px;
  max-height: 52vh;
  overflow-y: auto;
}

.media-pick {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 6px;
  font: inherit;
  font-size: 11.5px;
  text-align: left;
  color: var(--text);
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.media-pick:hover { border-color: var(--border-strong); background: var(--bg-sunken); }
.media-pick img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 2px;
  background: var(--bg-sunken);
}
.media-pick-name { word-break: break-all; line-height: 1.3; }
/* Selection is a ring PLUS the same drawn tick the media grid uses, so it is
   never carried by colour alone. */
.media-pick.selected { border-color: var(--accent-border); box-shadow: 0 0 0 2px var(--accent); }
.media-pick.selected .media-pick-name::after {
  content: '';
  display: inline-block;
  width: 5px;
  height: 9px;
  margin: 0 0 1px 6px;
  vertical-align: middle;
  border: solid var(--success-text);
  border-width: 0 2px 2px 0;
  transform: rotate(40deg);
}

/* ── post list: in-row status control ─────────────────────────────────── */

.row-status-control { display: flex; align-items: center; gap: 6px; margin-top: 6px; flex-wrap: wrap; }
/* The <select> and its dot travel together — see the note in posts/list.ejs. */
.row-status-line { display: flex; align-items: center; gap: 6px; flex-wrap: nowrap; }

/* Status dot — sits immediately right of the status <select>, which replaced the
   old pill in this row (the dropdown already states the status in words, so the
   pill was saying it twice).
   ⚠️ This is the ONE place in the admin where colour alone carries the state,
   and it is only acceptable because the <select> beside it names that state in
   words, in the same flex row, at every width. It also carries its own
   title/aria-label as a backstop. If the dot is ever separated from that
   control, it needs a text label of its own.
   CSS-drawn, like every other shape here — no font glyph. */
.status-dot {
  flex: none;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--dot-colour, var(--st-draft-bd));
  /* ⚠️ The ring is not decoration on the yellow dot — it is what makes the shape
     visible. A true yellow (#FFB300) is 1.79:1 against a white row, so the FILL
     cannot satisfy 1.4.11; the ring at hsl(42 100% 30%) is 4.71:1 and does. The
     other three fills clear 3:1 on their own and are ringed only for
     consistency. In dark the fills carry it (3.87–9.40:1) and the ring reads as
     a quiet edge. */
  border: 1px solid var(--dot-ring, var(--dot-colour, var(--st-draft-bd)));
}
.status-dot-draft { --dot-colour: var(--st-draft-bd); --dot-ring: var(--dot-ring-draft); }
.status-dot-needs_review { --dot-colour: var(--st-review-bg); --dot-ring: var(--dot-ring-review); }
.status-dot-scheduled { --dot-colour: var(--st-sched-bd); --dot-ring: var(--dot-ring-sched); }
.status-dot-published { --dot-colour: var(--st-pub-bd); --dot-ring: var(--dot-ring-pub); }
.row-schedule { display: inline-flex; align-items: center; gap: 6px; }
.row-schedule[hidden] { display: none; }
.row-schedule input { font-size: 12px; padding: 5px 7px; width: auto; }
.scheduled-when { font-size: 11.5px; }

/* ── dashboard ────────────────────────────────────────────────────────── */
/* Flat white tiles with a coloured left border and a matching icon. NOT
   gradient pills: the NUMBER is the thing being read, so it gets the visual
   weight and the colour stays a quiet index down the left edge.

   Tile accent colours are not decoration — each one is the status colour of the
   thing it counts (draft grey, review amber, scheduled blue, published green),
   so a tile and its badge in the list below agree. Tiles that count something
   with no status (posts, pages, media, leads) use the neutral accent. */

.stat-grid {
  display: grid;
  /* EXPLICIT column counts, not auto-fill. With auto-fill the eight tiles packed
     as 5 + 3 at desktop width, which reads as a mistake; a fixed count gives an
     even grid at every breakpoint. minmax(0, 1fr) rather than 1fr so a long
     unbreakable label cannot push a column wider than its share. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  /* Every ROW the same height as the tallest tile in the grid. Together with the
     min-height on .stat-card this holds the grid square at any width — if a
     label ever takes a third line, every row grows together rather than one. */
  grid-auto-rows: 1fr;
  gap: 12px;
  margin: 0 0 22px;
}

/* 900px is the sidebar breakpoint (the nav becomes a horizontal bar there), so
   the content area jumps ~216px wider at exactly that point — the tile counts
   are pinned to the same value to stay in step with it. */
@media (max-width: 900px) {
  .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .stat-grid { grid-template-columns: minmax(0, 1fr); }
}

.stat-card {
  display: grid;
  grid-template-columns: 36px 1fr;
  grid-template-rows: auto auto;
  column-gap: 12px;
  row-gap: 2px;
  align-items: center;
  /* Every tile the same height whatever its label does. The min-height sets the
     floor and align-content centres the two rows within it, so a one-line label
     and a two-line label ("Leads · 0 in the last 7 days") produce identical
     boxes instead of a ragged grid. */
  align-content: center;
  /* 84px is the natural height of the content — 26px number + a two-line label +
     12px padding top and bottom — so a two-line label fits without growing the
     tile, and a one-line label does not shrink it (the label reserves two lines).
     Down from 96px because the tiles are much wider at a fixed 4-across. */
  min-height: 84px;
  padding: 12px 16px;
  text-decoration: none;
  color: var(--tile-fg);
  background: var(--tile-bg, var(--tile-neutral-bg));
  border: 1px solid transparent;
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
}
.stat-card:hover { filter: brightness(1.08); }
.stat-card:focus-visible { outline-offset: 3px; }

/* The icon sits on a translucent wash of the tile's own fill, so it reads as
   part of the tile rather than as a second colour. aria-hidden in the template —
   the label beside it carries the meaning. */
.stat-icon {
  grid-row: 1 / span 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-sm);
  background: var(--tile-icon-wash);
  color: var(--tile-fg);
}
.stat-icon svg { width: 18px; height: 18px; display: block; }

.stat-value {
  font-variant-numeric: tabular-nums;
  font-size: 26px;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: var(--tile-fg);
}
.stat-label {
  font-size: 11.5px;
  /* Bold for legibility on the lower-contrast fills. ⚠️ It does NOT relax the
     contrast requirement: WCAG's large-text allowance needs 14pt bold (~18.5px)
     and this is 11.5px, so 4.5:1 still applies — see scripts/check-contrast.js. */
  font-weight: 600;
  color: var(--tile-fg-dim);
  line-height: 1.35;
  /* Reserve two lines so a short label occupies the same space as a wrapped one. */
  min-height: calc(1.35em * 2);
}

/* Per-tile fill. A STATUS tile takes the hue of its badge; a COUNT tile takes
   its own colour for variety. Both themes resolve through the same token names,
   so a tile can never be a colour the matching badge is not. */
.stat-card.tile-draft { --tile-bg: var(--tile-draft-bg); }
/* ⚠️ The ONLY tile with dark text, and it has to be. A true yellow cannot carry
   white text at a readable ratio — white on #FFB300 is 1.79:1, nowhere near the
   4.5:1 this 11.5px label needs, and darkening the fill until white worked would
   stop it being yellow. Dark text gets 9.88:1. Do not "fix" this back to white
   for consistency with the other seven tiles. */
.stat-card.tile-review {
  --tile-bg: var(--tile-review-bg);
  --tile-fg: var(--tile-review-fg);
  --tile-fg-dim: var(--tile-review-fg-dim);
}
.stat-card.tile-scheduled { --tile-bg: var(--tile-sched-bg); }
.stat-card.tile-posts { --tile-bg: var(--tile-posts-bg); }
.stat-card.tile-pages { --tile-bg: var(--tile-pages-bg); }
.stat-card.tile-media { --tile-bg: var(--tile-media-bg); }
.stat-card.tile-leads { --tile-bg: var(--tile-leads-bg); }
.stat-card.tile-inbox { --tile-bg: var(--tile-inbox-bg); }

.dash-columns {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: var(--gap);
}
.dash-panel { padding: 16px 18px; }
.dash-panel h2 { margin: 0 0 10px; font-size: 15px; font-weight: 600; }
.dash-list { margin: 0; padding: 0; list-style: none; font-size: 13px; }
.dash-list li {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding: 7px 0;
  border-bottom: 1px solid var(--border);
}
.dash-list li:last-child { border-bottom: 0; }
.dash-list .muted { font-size: 11.5px; }

/* ── theme toggle ─────────────────────────────────────────────────────── */
/* A real <button> in the sidebar: focusable, labelled in words, and reflecting
   its state through aria-pressed rather than through colour. The icon is
   CSS-drawn (half-filled circle) for the same reason the badges are — no font
   glyph, no combining mark. */

.theme-toggle {
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 8px 14px 0;
  padding: 8px 12px;
  font: inherit;
  font-size: 12.5px;
  color: var(--text-on-dark);
  background: transparent;
  border: 1px solid var(--text-on-dark-dim);
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-align: left;
}
.theme-toggle:hover { background: var(--bg-sidebar-hover); color: var(--text-on-dark-hi); }
.theme-toggle::before {
  content: '';
  flex: none;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  border: 1.5px solid currentColor;
  background: linear-gradient(90deg, currentColor 0 50%, transparent 50% 100%);
}
.theme-toggle-label { line-height: 1; }
