/* ExoPost — design system.
   Two-tier tokens: Tier 1 (--p-*) are raw color primitives and the ONLY
   thing a theme redefines — a theme is colors, nothing else. Tier 2 are
   semantic roles referenced by every component rule; most are derived
   from Tier 1 via color-mix(), so themes can't "forget" a token.
   Typography, spacing, radii, elevation, and iconography are fixed across
   all themes (one deliberate exception: Wire Desk's serif persona voice,
   --font-content). Add a theme = one Tier 1 block + one entry in theme.py. */

/* ── Fonts — self-hosted (no external domains, per project rule 1). ────── */
@font-face {
  font-family: "IBM Plex Sans";
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  src: url("vendor/fonts/ibm-plex-sans-400.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Sans";
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  src: url("vendor/fonts/ibm-plex-sans-500.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Sans";
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  src: url("vendor/fonts/ibm-plex-sans-600.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Sans";
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  src: url("vendor/fonts/ibm-plex-sans-700.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  src: url("vendor/fonts/ibm-plex-mono-400.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  src: url("vendor/fonts/ibm-plex-mono-500.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  src: url("vendor/fonts/ibm-plex-mono-600.woff2") format("woff2");
}

:root {
  /* ── Tier 1: color primitives ── default block = Linear. ──────────────── */
  --p-scheme: dark;
  --p-bg-0: #08090a;
  --p-bg-1: #0f1011;
  --p-bg-2: #17181a;
  --p-line: #26282b;
  --p-line-strong: #3a3d42;
  --p-accent: #5e6ad2;
  --p-accent-dim: #4a55b8;
  --p-accent-light: #b8bcc8;
  --p-accent-contrast: #ffffff;
  --p-text: #f7f8f8;
  --p-text-muted: #8a8f98;
  --p-danger: #eb5757;
  --p-danger-dim: #5c2a2a;
  --p-success: #4cb782;
  --p-success-dim: #26493a;
  --p-warning: #d99e3c;
  /* Informational blue — a state that is deliberate and fine, but not a
     success and not a brand accent. Defined on :root ONLY, deliberately:
     every theme block below overrides just the primitives it names, so an
     unlisted one falls through to here and this single blue serves all
     eight themes. That is right for this token where it would be wrong for
     the others — success/danger/warning are tuned per theme because they
     carry weight against very different surfaces, while "informational" is
     the one status that must NOT compete with a theme's own accent. A
     per-theme blue would drift toward each accent and lose the only job it
     has: reading as distinct from the brand color. Mid-blue so it holds up
     on both the dark and light schemes. */
  --p-info: #3b82f6;
  --p-shadow: rgba(0, 0, 0, 0.5);

  /* ── Fluent 2 color token structure — derived from Tier 1. ──────────────
     Fluent's own semantic slots (colorNeutralBackground1-4,
     colorNeutralForeground1-4, colorNeutralStroke1-2, colorBrand*,
     colorStatus*), populated per theme via the Tier 1 primitives above.
     Where a hand-tuned Tier 2 equivalent already existed (a `-dim` color,
     `accent-hover`, `-soft` tints), it's reused verbatim rather than
     reformulated, so this restructuring doesn't silently shift any color
     a component already renders. Hover/Pressed/Selected states this
     project never had are filled in via color-mix() to match Fluent's
     own contrast pattern: dark schemes lighten on hover and darken past
     the base on press; light schemes darken for both, press going
     furthest. This block assumes the default dark scheme; the light
     themes (Nordic Teal, Morning Wire) override the direction-sensitive
     entries in the shared light-scheme block below. */
  --colorNeutralBackground1: var(--p-bg-1);
  --colorNeutralBackground1Hover: color-mix(in srgb, var(--colorNeutralBackground1) 90%, white);
  --colorNeutralBackground1Pressed: color-mix(in srgb, var(--colorNeutralBackground1) 92%, black);
  --colorNeutralBackground1Selected: color-mix(in srgb, var(--colorNeutralBackground1) 94%, white);
  --colorNeutralBackground2: var(--p-bg-0);
  --colorNeutralBackground2Hover: color-mix(in srgb, var(--colorNeutralBackground2) 90%, white);
  --colorNeutralBackground2Pressed: color-mix(in srgb, var(--colorNeutralBackground2) 92%, black);
  --colorNeutralBackground2Selected: color-mix(in srgb, var(--colorNeutralBackground2) 94%, white);
  --colorNeutralBackground3: var(--p-bg-2);
  --colorNeutralBackground3Hover: color-mix(in srgb, var(--colorNeutralBackground3) 90%, white);
  --colorNeutralBackground3Pressed: color-mix(in srgb, var(--colorNeutralBackground3) 92%, black);
  --colorNeutralBackground3Selected: color-mix(in srgb, var(--colorNeutralBackground3) 94%, white);
  /* The one *recessed* surface: input wells. Sits between canvas and
     surface in both dark and light themes, so inputs always read as
     "a well you can type into" against their card. */
  --colorNeutralBackground4: color-mix(in srgb, var(--p-bg-0) 72%, var(--p-bg-1));
  --colorNeutralBackground4Hover: color-mix(in srgb, var(--colorNeutralBackground4) 90%, white);
  --colorNeutralBackground4Pressed: color-mix(in srgb, var(--colorNeutralBackground4) 92%, black);

  --colorNeutralForeground1: var(--p-text);
  --colorNeutralForeground2: var(--p-text-muted);
  --colorNeutralForeground2Hover: var(--colorNeutralForeground1);
  --colorNeutralForeground3: color-mix(in srgb, var(--p-text-muted) 72%, var(--p-bg-1));
  --colorNeutralForeground3Hover: var(--colorNeutralForeground2);
  --colorNeutralForeground4: color-mix(in srgb, var(--p-text-muted) 50%, var(--p-bg-1));
  --colorNeutralForegroundDisabled: color-mix(in srgb, var(--p-text-muted) 45%, var(--p-bg-1));

  /* Stroke1 is Fluent's *more* visible structural border; Stroke2 the
     subtle decorative hairline — matches this project's existing
     border-strong/border split exactly. */
  --colorNeutralStroke1: var(--p-line-strong);
  --colorNeutralStroke1Hover: color-mix(in srgb, var(--colorNeutralStroke1) 85%, white);
  --colorNeutralStroke2: var(--p-line);

  --colorBrandBackground: var(--p-accent);
  --colorBrandBackgroundHover: var(--p-accent-dim);
  --colorBrandBackgroundPressed: color-mix(in srgb, var(--p-accent-dim) 85%, black);
  --colorBrandBackgroundSelected: color-mix(in srgb, var(--p-accent) 88%, black);
  --colorBrandBackground2: color-mix(in srgb, var(--p-accent) 13%, var(--p-bg-1));
  --colorBrandForeground1: var(--p-accent);
  --colorBrandForeground2: var(--p-accent-dim);
  --colorBrandForegroundLink: var(--p-accent);
  --colorBrandForegroundLinkHover: var(--p-accent-dim);
  --colorBrandStroke1: var(--p-accent);
  --colorBrandStroke2: color-mix(in srgb, var(--p-accent) 30%, var(--p-line));
  /* Compound brand (e.g. a checked control's accent ring) mirrors the
     plain brand roles — nothing here needs a second palette. */
  --colorCompoundBrandForeground1: var(--colorBrandForeground1);
  --colorCompoundBrandForeground1Hover: var(--colorBrandForeground2);
  --colorCompoundBrandForeground1Pressed: color-mix(in srgb, var(--colorBrandForeground2) 80%, black);
  --colorCompoundBrandBackground: var(--colorBrandBackground);
  --colorCompoundBrandBackgroundHover: var(--colorBrandBackgroundHover);
  --colorCompoundBrandBackgroundPressed: var(--colorBrandBackgroundPressed);

  /* Status triads: Background1 (subtle tint) / 2 (medium) / 3 (solid),
     Foreground1 (the color already proven legible on a card — see the
     accessibility scan) / 2 (bolder emphasis), Border1 (subtle) /
     BorderActive (solid). Background1 and Border1 reuse the hand-tuned
     `-soft`/`-dim` tints where this project already authored one. */
  --colorStatusSuccessBackground1: color-mix(in srgb, var(--p-success) 14%, var(--p-bg-1));
  --colorStatusSuccessBackground2: var(--p-success-dim);
  --colorStatusSuccessBackground3: var(--p-success);
  --colorStatusSuccessForeground1: var(--p-success);
  --colorStatusSuccessForeground2: color-mix(in srgb, var(--p-success) 85%, white);
  --colorStatusSuccessBorder1: var(--p-success-dim);
  --colorStatusSuccessBorderActive: var(--p-success);

  /* Info: only the two slots anything actually renders. Deliberately not a
     full triad — the others exist because success/warning/danger are used
     as solid fills, borders and emphasis text across the app, and adding
     five unused tokens per status is how a palette stops being auditable. */
  --colorStatusInfoBackground1: color-mix(in srgb, var(--p-info) 14%, var(--p-bg-1));
  /* Lifted off the raw primitive, unlike the other three statuses, and the
     reason is that this is the one status color NOT tuned per theme. Each
     theme sets its own --p-success/-warning/-danger already legible against
     its own surfaces, so their Foreground1 can be the primitive itself. One
     fixed blue cannot do that: #3b82f6 on its own 14% tint measures 3.0-3.1
     on the light themes and 3.8-4.4 on the dark ones, and a pill is 10px
     bold uppercase — small text, so it owes 4.5. Derived direction-
     sensitively instead, which is the same mechanism this block already
     uses for hover/pressed states: lighten on dark (worst case 5.46),
     darken on light (worst case 4.93, overridden in the light-scheme block
     below). Measured, not eyeballed — recompute if the blue ever moves. */
  --colorStatusInfoForeground1: color-mix(in srgb, var(--p-info) 75%, white);

  --colorStatusWarningBackground1: color-mix(in srgb, var(--p-warning) 14%, var(--p-bg-1));
  --colorStatusWarningBackground2: color-mix(in srgb, var(--p-warning) 30%, var(--p-bg-1));
  --colorStatusWarningBackground3: var(--p-warning);
  --colorStatusWarningForeground1: var(--p-warning);
  --colorStatusWarningForeground2: color-mix(in srgb, var(--p-warning) 85%, white);
  --colorStatusWarningBorder1: color-mix(in srgb, var(--p-warning) 30%, var(--p-bg-1));
  --colorStatusWarningBorderActive: var(--p-warning);

  --colorStatusDangerBackground1: color-mix(in srgb, var(--p-danger) 14%, var(--p-bg-1));
  --colorStatusDangerBackground2: var(--p-danger-dim);
  --colorStatusDangerBackground3: var(--p-danger);
  --colorStatusDangerForeground1: var(--p-danger);
  --colorStatusDangerForeground2: color-mix(in srgb, var(--p-danger) 85%, white);
  --colorStatusDangerBorder1: var(--p-danger-dim);
  --colorStatusDangerBorderActive: var(--p-danger);

  /* ── Tier 2: semantic roles — what component CSS references. Now thin
     aliases onto the Fluent layer above, so every existing component
     rule keeps working unchanged. ──────────────────────────────────── */
  /* Surfaces (3-level depth stack: canvas → surface → raised/overlay) */
  --bg-canvas: var(--colorNeutralBackground2);
  --bg-surface: var(--colorNeutralBackground1);
  --bg-raised: var(--colorNeutralBackground3);
  --bg-overlay: var(--colorNeutralBackground3);
  --bg-inset: var(--colorNeutralBackground4);

  /* Borders — decorative hairlines vs. structural */
  --border: var(--colorNeutralStroke2);
  --border-strong: var(--colorNeutralStroke1);

  /* Text */
  --text-primary: var(--colorNeutralForeground1);
  --text-secondary: var(--colorNeutralForeground2);
  --text-tertiary: var(--colorNeutralForeground3);
  --text-on-accent: var(--p-accent-contrast);

  /* Accent + status roles. --*-soft are tint backgrounds guaranteed to
     pair with their own foreground role — never mix pairs across roles
     (see the .chip.source contrast lesson in exopost-architecture.md). */
  --accent: var(--colorBrandBackground);
  --accent-hover: var(--colorBrandBackgroundHover);
  --accent-soft: var(--colorBrandBackground2);
  --accent-light: var(--p-accent-light);
  --accent-contrast: var(--p-accent-contrast);
  --success: var(--colorStatusSuccessForeground1);
  --success-soft: var(--colorStatusSuccessBackground1);
  --danger: var(--colorStatusDangerForeground1);
  --danger-soft: var(--colorStatusDangerBackground1);
  --warning: var(--colorStatusWarningForeground1);
  --warning-soft: var(--colorStatusWarningBackground1);
  --info: var(--colorStatusInfoForeground1);
  --info-soft: var(--colorStatusInfoBackground1);
  --ticket-title: var(--p-ticket-title, var(--p-accent-light));

  /* Inputs — the findability contract: the well is recessed relative to
     its card, and the input border is always visibly stronger than the
     decorative hairline (--border). */
  --input-bg: var(--bg-inset);
  --input-border: color-mix(in srgb, var(--p-line-strong) 60%, var(--p-line));
  --input-border-hover: color-mix(in srgb, var(--accent) 45%, var(--input-border));
  --input-border-focus: var(--accent);
  --input-placeholder: color-mix(in srgb, var(--text-secondary) 62%, transparent);
  --focus-ring: color-mix(in srgb, var(--accent) 28%, transparent);
  --danger-ring: color-mix(in srgb, var(--danger) 28%, transparent);

  /* Elevation */
  --shadow-sm: 0 1px 3px var(--p-shadow);
  --shadow-md: 0 6px 20px var(--p-shadow);
  --shadow-lg: 0 20px 48px var(--p-shadow);

  /* ── Fixed system scales (never themed) ───────────────────────────────── */
  --font-display: "IBM Plex Sans", -apple-system, "Segoe UI", sans-serif;
  --font-body: "IBM Plex Sans", -apple-system, "Segoe UI", sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;
  /* Persona-voice content (ticket previews, dispatch body). Defaults to
     body; Wire Desk overrides with its serif — content voice, not UI. */
  --font-content: var(--font-body);

  --text-xs: 11px;
  --text-sm: 12.5px;
  --text-base: 14px;
  --text-md: 16px;
  --text-lg: 20px;
  --text-xl: 26px;

  /* Functional signal colors for the live connection/polling dots —
     green = connected, red = down. Deliberately NOT themed: these two
     always mean the same thing, so they read identically in every theme
     (unlike --success/--danger, which themes tune to their palette). */
  --signal-ok: #23c55e;
  --signal-down: #ef4444;
  /* Third signal: a manually-granted state that isn't backed by the usual
     mechanism (e.g. a tenant plan assigned in admin with no live Stripe
     subscription behind it) — not wrong like red, not fully verified like
     green. Same "deliberately not themed" reasoning as the two above. */
  --signal-warn: #d97706;
  /* Fourth signal: a deliberate internal/staff state — like green in that
     nothing is wrong, unlike green in that no subscription backs it and
     none ever will. Aliases --p-info rather than declaring a second blue,
     since that primitive is itself unthemed and this is the same color
     doing the same job one component over. */
  --signal-staff: var(--p-info);

  /* The range handle, which carries its own value inside it. Deliberately
     NOT themed, for the same reason as the signal colors above: the handle
     has to stay a dark disc with a light number on it in all eight themes,
     because that pairing is what makes the number legible at 12px inside a
     30px circle. Deriving it from a surface token would invert on the three
     light themes and put light-grey text on a near-white disc. The border
     is themed, and is what separates the disc from the track on the dark
     themes where the two are close in value. */
  --range-thumb: #14161a;
  --range-thumb-text: #f7f8f8;

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;
  --radius-sm: 4px;
  --radius-md: 6px;
  --radius-lg: 10px;

  /* ── Legacy aliases — old names resolving through Tier 2. Kept so any
     straggler rule/template keeps theming; do not use in new CSS. ──────── */
  --ink: var(--bg-canvas);
  --surface: var(--bg-surface);
  --surface-raised: var(--bg-raised);
  --line: var(--border);
  --amber: var(--accent);
  --amber-dim: var(--accent-hover);
  --amber-light: var(--accent-light);
  --fg: var(--text-primary);
  --fg-muted: var(--text-secondary);
  --text: var(--text-primary);
  --text-muted: var(--text-secondary);
  --red: var(--danger);
  --red-dim: var(--p-danger-dim);
  --green: var(--success);
  --green-dim: var(--p-success-dim);
  --success-dim: var(--p-success-dim);
  --danger-dim: var(--p-danger-dim);
  --accent-dim: var(--accent-hover);
  --border-focus: var(--input-border-focus);
  --mono: var(--font-body);
  --serif: var(--font-content);
  --font-ui: var(--font-body);
  --fs-xs: var(--text-xs);
  --fs-sm: var(--text-sm);
  --fs-md: var(--text-md);
  --shadow-1: var(--shadow-sm);
  --shadow-2: var(--shadow-md);
}

/* ── Themes — colors only. ────────────────────────────────────────────── */

/* Wire Desk — the amber wire-terminal identity. Keeps its serif for the
   persona's rendered voice (--font-content) — the one non-color theme
   exception, and it applies to content, never UI chrome. */
:root[data-theme="wire-desk"] {
  --p-bg-0: #14120d;
  --p-bg-1: #1e1b14;
  --p-bg-2: #26221a;
  --p-line: #3a3427;
  --p-line-strong: #554831;
  --p-accent: #e8a33d;
  --p-accent-dim: #c98a2e;
  --p-accent-light: #b99c73;
  --p-accent-contrast: #14120d;
  --p-text: #ede6d8;
  --p-text-muted: #9c9282;
  --p-danger: #d05a4f;
  --p-danger-dim: #6e2c27;
  --p-success: #6b9e78;
  --p-success-dim: #3d5943;
  --p-warning: #e8a33d;
  --p-shadow: rgba(0, 0, 0, 0.45);
  --font-content: "Iowan Old Style", "Palatino Linotype", "Book Antiqua",
    Georgia, serif;
}

/* Obsidian Mint — ultra-sharp, low-fatigue dark mode */
:root[data-theme="obsidian-mint"] {
  --p-bg-0: #08090c;
  --p-bg-1: #0f1117;
  --p-bg-2: #171a22;
  --p-line: #222633;
  --p-line-strong: #353b4f;
  --p-accent: #00f5a0;
  --p-accent-dim: #00d488;
  --p-accent-light: #a3ffd6;
  --p-accent-contrast: #08090c;
  --p-text: #ecf0f6;
  --p-text-muted: #798396;
  --p-danger: #ff5252;
  --p-danger-dim: #3a1414;
  --p-success: #00f5a0;
  --p-success-dim: #0c3321;
  --p-warning: #fcd34d;
  --p-shadow: rgba(0, 0, 0, 0.65);
}

/* Steel Slate — professional, balanced dark mode with deep cool tones */
:root[data-theme="steel-slate"] {
  --p-bg-0: #0b0d10;
  --p-bg-1: #13161c;
  --p-bg-2: #1b1f27;
  --p-line: #252b36;
  --p-line-strong: #3d4657;
  --p-accent: #58a6ff;
  --p-accent-dim: #388bfd;
  --p-accent-light: #c9d1d9;
  --p-accent-contrast: #0b0d10;
  --p-text: #f0f6fc;
  --p-text-muted: #8b949e;
  --p-danger: #f85149;
  --p-danger-dim: #3e1614;
  --p-success: #3fb950;
  --p-success-dim: #0e3115;
  --p-warning: #f0883e;
  --p-shadow: rgba(0, 0, 0, 0.55);
  --p-ticket-title: var(--p-accent);
}

/* Exo Teal — matches the exopost-landing marketing site's dark palette
   (public/css/style.css's default :root), so the app and the site read as
   one product instead of two different dark themes under one name. */
:root[data-theme="exo-teal"] {
  --p-bg-0: #121212;
  --p-bg-1: #1a1e1d;
  --p-bg-2: #262a2a;
  --p-line: #242827;
  --p-line-strong: #363b3a;
  --p-accent: #17a676;
  --p-accent-dim: #1cc089;
  --p-accent-light: #8fe8c9;
  --p-accent-contrast: #ffffff;
  --p-text: #ffffff;
  --p-text-muted: #b9c9c2;
  --p-danger: #ff3366;
  --p-danger-dim: #3a0f1a;
  --p-success: #37c48a;
  --p-success-dim: #0f3326;
  --p-warning: #ffbb00;
  --p-shadow: rgba(0, 0, 0, 0.6);
}

/* Nordic Teal — high-contrast, paper-clean light theme. Same token
   contract, inverted polarity: the input well is dimmer than the white
   card, and shadows are slate-tinted spread rather than darkness. */
:root[data-theme="nordic-teal"] {
  --p-scheme: light;
  --p-bg-0: #eef1f4;
  --p-bg-1: #ffffff;
  --p-bg-2: #e9edf1;
  --p-line: #dde1e5;
  --p-line-strong: #aeb6bf;
  --p-accent: #0f766e;
  --p-accent-dim: #115e59;
  --p-accent-light: #99cfca;
  --p-accent-contrast: #ffffff;
  --p-text: #0f172a;
  --p-text-muted: #475569;
  --p-danger: #dc2626;
  --p-danger-dim: #fecaca;
  --p-success: #15803d;
  --p-success-dim: #bbe7c9;
  --p-warning: #c2530a;
  --p-shadow: rgba(15, 23, 42, 0.1);
  --p-ticket-title: #0d5f59;
  /* Bespoke Fluent-slot values — this theme's tertiary text and inset
     well don't fit the generic Tier-1-derived formula, same as before
     the Fluent restructuring (previously overridden as --text-tertiary /
     --bg-inset directly; now overridden one layer down, at the Fluent
     slots those aliases read from). */
  --colorNeutralForeground3: #64748b;
  --colorNeutralBackground4: #f7f9fa;
  --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06), 0 1px 4px rgba(15, 23, 42, 0.05);
  --shadow-md: 0 4px 16px rgba(15, 23, 42, 0.1);
  --shadow-lg: 0 16px 44px rgba(15, 23, 42, 0.16);
}

/* Morning Wire — the daylight counterpart to Wire Desk: warm paper
   grounds, warm ink text, press-navy accent, brick-red danger. Same
   inverted input polarity as Nordic Teal (well dimmer than the card). */
:root[data-theme="morning-wire"] {
  --p-scheme: light;
  --p-bg-0: #f3f1ec;
  --p-bg-1: #fdfcfa;
  --p-bg-2: #ebe8e1;
  --p-line: #e2ded5;
  --p-line-strong: #b5aca0;
  --p-accent: #2d5a92;
  --p-accent-dim: #234a7a;
  --p-accent-light: #9fb8d4;
  --p-accent-contrast: #ffffff;
  --p-text: #26221b;
  --p-text-muted: #6b6353;
  --p-danger: #b3362c;
  --p-danger-dim: #ecd0cc;
  --p-success: #3f7d34;
  --p-success-dim: #d5e6cf;
  --p-warning: #a8630a;
  --p-shadow: rgba(62, 52, 32, 0.12);
  --p-ticket-title: #24487a;
  --colorNeutralForeground3: #8a8171;
  --colorNeutralBackground4: #f2efe8;
  --shadow-sm: 0 1px 2px rgba(62, 52, 32, 0.07), 0 1px 4px rgba(62, 52, 32, 0.05);
  --shadow-md: 0 4px 16px rgba(62, 52, 32, 0.11);
  --shadow-lg: 0 16px 44px rgba(62, 52, 32, 0.17);
}

/* Office — light, cold, Microsoft 365 blue. The accent and status colors
   are still lifted straight from @fluentui/tokens' real webLightTheme
   (colorBrandBackground/Hover, the status-palette green/orange/red). The
   neutrals are NOT Fluent's literal Background1/2/3/Stroke1/2 values,
   though — those are pure equal-channel grays (#fafafa, #f5f5f5, #424242,
   R=G=B), and pure neutral gray next to pure white reads as warm/muddy
   rather than cold, a real optical-mixing effect, not a rendering bug.
   "Cold" needs a deliberate cool (blue-leaning, B>G>R) bias on every gray
   step instead — same trick Nordic Teal already uses — while the accent
   carries the actual Microsoft blue so the theme still reads as Office,
   not as Nordic Teal's teal. */
:root[data-theme="office"] {
  --p-scheme: light;
  --p-bg-0: #e9f1f8;
  --p-bg-1: #ffffff;
  --p-bg-2: #dfe9f2;
  --p-line: #cfdce8;
  --p-line-strong: #9db3c7;
  --p-accent: #0f6cbd;
  --p-accent-dim: #115ea3;
  --p-accent-light: #b4d6fa;
  --p-accent-contrast: #ffffff;
  --p-text: #22344a;
  --p-text-muted: #51677f;
  --p-danger: #c50f1f;
  --p-danger-dim: #eeacb2;
  --p-success: #107c10;
  --p-success-dim: #9fd89f;
  --p-warning: #f7630c;
  --p-shadow: rgba(15, 35, 60, 0.14);
  /* Fluent's colorBrandBackgroundPressed — a deeper navy than the plain
     accent, same role Nordic Teal/Morning Wire use a bespoke ticket-title
     for rather than falling back to the (low-contrast-as-text) accent-light. */
  --p-ticket-title: #0c3b5e;
  --shadow-sm: 0 1px 2px rgba(36, 36, 36, 0.06), 0 1px 4px rgba(36, 36, 36, 0.05);
  --shadow-md: 0 4px 16px rgba(36, 36, 36, 0.1);
  --shadow-lg: 0 16px 44px rgba(36, 36, 36, 0.16);
}

/* Light-scheme direction fix — Fluent's real light themes darken on
   hover and press further still; dark themes (the :root default above)
   lighten on hover and darken past the base on press. Same slots,
   opposite color-mix() target, so the light themes get Fluent's actual
   light-theme contrast pattern instead of inheriting the dark one
   upside down. */
:root[data-theme="nordic-teal"],
:root[data-theme="morning-wire"],
:root[data-theme="office"] {
  --colorNeutralBackground1Hover: color-mix(in srgb, var(--colorNeutralBackground1) 96%, black);
  --colorNeutralBackground1Pressed: color-mix(in srgb, var(--colorNeutralBackground1) 88%, black);
  --colorNeutralBackground1Selected: color-mix(in srgb, var(--colorNeutralBackground1) 92%, black);
  --colorNeutralBackground2Hover: color-mix(in srgb, var(--colorNeutralBackground2) 96%, black);
  --colorNeutralBackground2Pressed: color-mix(in srgb, var(--colorNeutralBackground2) 88%, black);
  --colorNeutralBackground2Selected: color-mix(in srgb, var(--colorNeutralBackground2) 92%, black);
  --colorNeutralBackground3Hover: color-mix(in srgb, var(--colorNeutralBackground3) 96%, black);
  --colorNeutralBackground3Pressed: color-mix(in srgb, var(--colorNeutralBackground3) 88%, black);
  --colorNeutralBackground3Selected: color-mix(in srgb, var(--colorNeutralBackground3) 92%, black);
  --colorNeutralBackground4Hover: color-mix(in srgb, var(--colorNeutralBackground4) 96%, black);
  --colorNeutralBackground4Pressed: color-mix(in srgb, var(--colorNeutralBackground4) 88%, black);
  --colorNeutralStroke1Hover: color-mix(in srgb, var(--colorNeutralStroke1) 85%, black);
  --colorNeutralForegroundDisabled: color-mix(in srgb, var(--p-text-muted) 55%, var(--p-bg-1));
  --colorBrandBackgroundPressed: color-mix(in srgb, var(--colorBrandBackgroundHover) 80%, black);
  --colorBrandBackgroundSelected: color-mix(in srgb, var(--p-accent) 88%, white);
  --colorCompoundBrandForeground1Pressed: color-mix(in srgb, var(--colorBrandForeground2) 80%, white);
  --colorStatusSuccessForeground2: color-mix(in srgb, var(--p-success) 80%, black);
  --colorStatusWarningForeground2: color-mix(in srgb, var(--p-warning) 80%, black);
  --colorStatusDangerForeground2: color-mix(in srgb, var(--p-danger) 80%, black);
  /* Foreground1, not 2 — the pill renders Foreground1, and info is the one
     status whose primitive isn't theme-tuned, so the light themes need the
     direction flipped or the badge sits at ~3.1 against its own tint. */
  --colorStatusInfoForeground1: color-mix(in srgb, var(--p-info) 75%, black);
}

/* ── Base ─────────────────────────────────────────────────────────────── */

* {
  box-sizing: border-box;
}

html {
  color-scheme: var(--p-scheme);
}

html.theme-transition,
html.theme-transition body,
html.theme-transition .sidebar,
html.theme-transition .content {
  transition: background-color 0.18s ease, color 0.18s ease,
    border-color 0.18s ease;
}

/* The `hidden` attribute, restored.

   The browser's own [hidden] rule is a UA-stylesheet default, so ANY author
   rule that sets a display beats it — and .field sets display:flex. A field
   marked hidden in the template therefore rendered normally, which is how
   the X-only character-limit control showed up on a Bluesky campaign: the
   JS toggling it was correct and had always been correct, while the attribute
   it toggles did nothing.

   !important because the competing rules (.field, .combobox__option, …) are
   all more specific than a bare attribute selector, so there is no
   specificity this can win on. Global rather than per-component: the
   alternative is noticing every time a new component sets its own display
   and hiding it again by hand, which .combobox__option[hidden] had already
   had to do once — that local patch is removed, since this replaces it. */
[hidden] {
  display: none !important;
}

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

a {
  color: var(--accent);
}

code {
  font-family: var(--font-mono);
  font-size: 0.92em;
}

.icon {
  flex-shrink: 0;
  vertical-align: -0.18em;
}

/* Keyboard focus — visible everywhere, on top of any component styling. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}

/* ── Headings ─────────────────────────────────────────────────────────── */

h1 {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.3;
  color: var(--text-primary);
  margin: 0 0 var(--space-2);
}

h2 {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--text-primary);
  margin: var(--space-8) 0 var(--space-2);
}

h2:first-child {
  margin-top: 0;
}

/* Uniform page-title block: 20px display title + one-line description,
   optional right-aligned actions. */
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-bottom: var(--space-6);
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--border);
}

.page-header__text {
  min-width: 0;
}

.page-header__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.page-header h1 {
  margin-bottom: 2px;
}

.page-desc,
.desc {
  font-size: var(--text-base);
  line-height: 1.6;
  color: var(--text-secondary);
  margin: 0;
}

/* Micro-caps: reserved for non-interactive data labels (stats, table
   headers, nav groups) — form labels deliberately do NOT look like this. */
.stat__label,
th,
.nav-section-label,
.info-bar-label,
.slider-group-label,
.wire-facts .label {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-tertiary);
}

/* ── Shell layout ─────────────────────────────────────────────────────── */

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

.content {
  flex: 1;
  min-width: 0;
  padding: var(--space-8) var(--space-10);
  max-width: 1200px;
}

/* ── Sidebar ──────────────────────────────────────────────────────────── */

.sidebar {
  width: 224px;
  flex-shrink: 0;
  background: var(--bg-surface);
  border-right: 1px solid var(--border);
  padding: var(--space-6) var(--space-3);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}

.brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 0 var(--space-2) var(--space-3);
  margin-bottom: var(--space-2);
  border-bottom: 1px solid var(--border);
}

.brand__logo {
  max-width: 180px;
  width: 100%;
  height: auto;
  display: block;
}

/* variable_curve tracks the theme accent; static_curve + the wordmark
   track the theme's primary text color, so both flip automatically
   across every named theme, not just light/dark. */
.logo-accent { fill: var(--accent); }
.logo-ink { fill: var(--text-primary); }

.brand .brand__mark {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.1em;
  color: var(--accent);
  text-transform: uppercase;
}

.nav-section {
  display: flex;
  flex-direction: column;
  gap: 1px;
  margin-bottom: var(--space-2);
}

.nav-section-label {
  padding: 0 var(--space-2);
  margin-bottom: 2px;
}

.nav-link {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 5px var(--space-2);
  border-radius: var(--radius-md);
  color: var(--text-secondary);
  text-decoration: none;
  font-size: 13px;
  font-weight: 500;
}

.nav-link .icon {
  color: var(--text-tertiary);
}
/* Keeps a trailing brand mark tight against its label ("Top up 𝕏") — the
   nav's own 10px flex gap is spacing between an icon and its item, and
   reads as a gap in the phrase when it lands mid-label. */
.nav-link__label {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

.nav-link:hover {
  color: var(--text-primary);
  background: var(--bg-raised);
}

.nav-link:hover .icon {
  color: var(--text-secondary);
}

.nav-link.active {
  color: var(--accent);
  background: var(--accent-soft);
}

.nav-link.active .icon {
  color: var(--accent);
}

.sidebar-spacer {
  flex: 1;
}

.workspace-pill {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  padding: 3px 3px 3px 8px;
  margin-bottom: var(--space-2);
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  background: var(--surface-raised);
  color: var(--text-secondary);
  font-size: 11px;
  font-weight: 500;
  line-height: 1;
  width: 100%;
  box-sizing: border-box;
}

.workspace-pill__icon {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--text-on-accent);
}

.workspace-pill__icon .icon {
  width: 13px;
  height: 13px;
}

.workspace-pill span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: 1;
}

/* Log out lives inside the workspace pill rather than as its own button at
   the bottom of the sidebar — one identity control instead of two. */
.workspace-pill__logout-form {
  margin-left: auto;
  flex-shrink: 0;
  display: flex;
}

.workspace-pill__logout {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text-tertiary);
  cursor: pointer;
  border-radius: var(--radius-sm, 6px);
}

.workspace-pill__logout:hover {
  color: var(--danger);
  background: var(--bg-inset);
}

/* "Profiles" label sits directly on the sidebar rather than inside a
   .nav-section — the switcher below it isn't a nav link group, just a
   labelled control, so it borrows the same micro-caps look without the
   section wrapper's list semantics. */
.sidebar > .nav-section-label {
  margin-top: var(--space-1);
}

.profile-switcher-row {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: var(--space-2);
}

.profile-switcher {
  flex: 1;
  min-width: 0;
}

.profile-switcher select,
.theme-switcher select {
  width: 100%;
  font-size: var(--text-sm);
  padding: 6px var(--space-2);
  height: 32px;
}

/* Deliberately a bordered square button, not a link styled like a nav item
   — it sits right next to the profile it would otherwise be mistaken for,
   so it reads as "add" through shape and color rather than wording. */
.profile-add-btn {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-md);
  color: var(--accent);
  text-decoration: none;
}

.profile-add-btn:hover {
  border-style: solid;
  border-color: var(--accent);
  background: var(--accent-soft);
}

.new-profile-link {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--accent);
  text-decoration: none;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-md);
  padding: 6px var(--space-2);
  margin-bottom: var(--space-3);
}

.new-profile-link:hover {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.theme-switcher {
  margin-bottom: var(--space-2);
}

.theme-switcher-label {
  display: block;
  font-size: var(--text-xs);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin-bottom: var(--space-1);
  padding: 0 2px;
}

.logout-form button {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  background: none;
  border: 1px solid var(--border);
  color: var(--text-secondary);
  padding: 7px var(--space-2);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  border-radius: var(--radius-md);
  cursor: pointer;
}

.logout-form button:hover {
  border-color: var(--danger);
  color: var(--danger);
  background: var(--danger-soft);
}

.app-version {
  text-align: center;
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-tertiary);
  margin-top: var(--space-2);
}

/* ── Tabs ─────────────────────────────────────────────────────────────── */

.tabs {
  display: flex;
  gap: var(--space-1);
  margin-bottom: var(--space-6);
  border-bottom: 1px solid var(--border);
}

.tab {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 9px 14px;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-secondary);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  margin-bottom: -1px;
  text-transform: capitalize;
}

.tab:hover {
  color: var(--text-primary);
  background: var(--bg-raised);
}

.tab.active {
  color: var(--accent);
  border-bottom-color: var(--accent);
  font-weight: 600;
}

/* tab--icon: a platform logo boxed in a small bordered square before the
   label — plain text next to X's bare glyph reads as a close/dismiss
   control, not a logo, so it always gets a frame. */
.tab--icon {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

.tab__icon-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
}

.tab--icon.active .tab__icon-badge {
  border-color: var(--accent);
  color: var(--accent);
}

/* ── Buttons ──────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  white-space: nowrap;
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 500;
  line-height: 1.4;
  padding: 10px 14px;
  border-radius: var(--radius-md);
  border: 1px solid var(--input-border);
  background: var(--bg-raised);
  color: var(--text-primary);
  cursor: pointer;
  white-space: nowrap;
  text-decoration: none;
  transition: border-color 0.12s ease, background-color 0.12s ease,
    color 0.12s ease;
}

.btn:hover {
  border-color: var(--input-border-hover);
  background: color-mix(in srgb, var(--bg-raised) 80%, var(--bg-surface));
}

/* The one action per form that actually submits it. */
.btn-primary {
  background: var(--accent);
  border-color: transparent;
  color: var(--text-on-accent);
}

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

.btn-quiet {
  background: none;
  border-color: var(--input-border);
  color: var(--text-secondary);
}

.btn-quiet:hover {
  background: var(--bg-raised);
  border-color: transparent;
  color: var(--text-primary);
}

.btn-danger {
  background: none;
  border-color: color-mix(in srgb, var(--danger) 45%, transparent);
  color: var(--danger);
}

.btn-danger:hover {
  background: var(--danger);
  border-color: var(--danger);
  color: var(--accent-contrast);
}

.btn-small {
  padding: 5px 10px;
  font-size: var(--text-sm);
}

/* Two/three-way radio choice styled as a pill toggle instead of native
   radio buttons — first use is the X character-limit tier (Free/Pro), but
   generic on purpose. The <input>s stay in the DOM and keep working with
   JS off (a real radio group, just visually hidden — same technique as
   .upload-row's is-upload-managed), with the label doing the clicking via
   its `for`. */
.segmented {
  display: inline-flex;
  border: 1px solid var(--input-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  width: fit-content;
}

/* Equal-width variant, for a STACK of segmented controls that read as one
   panel — the persona editor's four tone axes. The default sizes each
   control to its own labels, which is right for a lone control but wrong
   here: "Casual/Balanced/Formal" and "Measured/Balanced/Enthusiastic" then
   produce four rows of four different widths with their dividers landing in
   four different places, and the eye reads that as four unrelated controls
   rather than one set of parallel choices. Equal thirds of a shared width
   makes the selected position comparable down the column at a glance. */
.segmented--equal {
  display: flex;
  width: 100%;
  max-width: 460px;
}

.segmented--equal label,
.segmented--equal a {
  flex: 1 1 0;
  text-align: center;
  /* Without this, a long label ("Enthusiastic") forces its own segment wider
     than its siblings even inside a flex row, because the default min-width
     of a flex item is its content size. */
  min-width: 0;
}

/* Link variant. The Queue's destination filter is the same control as the
   persona editor's tone axes — one row, mutually exclusive, all options
   visible — but it navigates rather than posting, so its segments are <a>
   and the selected one is marked with .is-active instead of :checked. Same
   rules, one extra selector each, rather than a second control that looks
   almost like this one. */
.segmented a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  text-decoration: none;
}

.segmented a.is-active {
  background: var(--accent);
  color: var(--text-on-accent);
}

.segmented a:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--focus-ring);
}

/* The Queue's own spacing: the status tabs below carry the boundary line, so
   this sits just above them rather than being separated from them. */
.queue-destinations {
  margin-bottom: var(--space-3);
}

/* Wider than the persona editor's stack: up to five segments, each carrying
   a logo, a name and a count. */
.segmented--wide {
  max-width: 640px;
}

/* Count badge — a filled pill carrying a number inside the control it
   belongs to, rather than a figure floating beside its label. Lives on the
   Queue's status tabs; the destination row above them deliberately carries
   none, since a count there would restate that row's own selection instead
   of describing what is in the queue. */
.count-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 9px;
  background: var(--bg-raised);
  color: var(--text-secondary);
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

/* An empty status keeps its number — "nothing failed" is worth reading at a
   glance — but recedes to an outline rather than reading as a notification. */
.count-badge--zero {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-tertiary);
}

.tab.active .count-badge {
  background: var(--accent);
  border-color: transparent;
  color: var(--text-on-accent);
}

.segmented input[type="radio"] {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.segmented label,
.segmented a {
  padding: 8px 14px;
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 500;
  color: var(--text-secondary);
  background: var(--input-bg);
  cursor: pointer;
  transition: background-color 0.12s ease, color 0.12s ease;
}

.segmented label:not(:last-of-type),
.segmented a:not(:last-child) {
  border-right: 1px solid var(--input-border);
}

.segmented label:hover,
.segmented a:not(.is-active):hover {
  background: var(--bg-raised);
}

.segmented input[type="radio"]:checked + label {
  background: var(--accent);
  color: var(--text-on-accent);
}

.segmented input[type="radio"]:focus-visible + label {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--focus-ring);
}

/* Deliberately the loudest button on the campaign form — the posting
   schedule is the one setting that silently controls whether anything goes
   out at all, and as a small secondary-styled button it went unnoticed.
   Full width and taller than a normal .btn so it reads as the header
   action of the schedule card above it, not just another form control. */
.btn-schedule {
  width: 100%;
  padding: 18px 22px;
  font-size: var(--text-md);
  font-weight: 700;
  border-radius: var(--radius-lg, 14px);
  background: var(--accent);
  border-color: transparent;
  color: var(--text-on-accent);
}

.btn-schedule:hover {
  background: var(--accent-hover);
  border-color: transparent;
}

/* The Weekly Schedule summary card — a colored day badge plus that day's
   times, one row per day that actually has a slot, rather than one chip
   per time entry. Grouped so a campaign with several times a day reads as
   a schedule instead of a bag of pills. */
.schedule-card {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg, 16px);
  padding: 20px;
  background: var(--bg-surface);
}

.schedule-card__title {
  text-align: center;
  font-weight: 700;
  font-size: 1.1rem;
  color: var(--text-primary);
  margin: 0 0 4px;
}

/* Recurrence status — the schedule card only ever showed times, not
   whether they repeat, and that's the one setting that decides whether the
   campaign keeps posting after this week or quietly stops. */
.schedule-card__status {
  text-align: center;
  font-size: var(--text-xs);
  font-weight: 600;
  margin: 0 0 14px;
}
.schedule-card__status.is-recurring { color: var(--accent); }
.schedule-card__status.is-once { color: var(--text-secondary); }

.schedule-card__divider {
  border: 0;
  border-top: 1px solid var(--border);
  margin: 0 0 14px;
}

.schedule-day-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 7px 0;
}

.schedule-day-badge {
  flex: 0 0 auto;
  min-width: 62px;
  border-radius: 999px;
  padding: 6px 16px;
  font-weight: 700;
  font-size: var(--text-sm);
  color: #fff;
  text-align: center;
}

/* Monday/Tuesday blue, Wednesday purple, Thursday orange, Friday red,
   weekend green — just enough color variety to tell rows apart at a
   glance, matched to the reference design. "Every day" entries (legacy
   bare "09:00" schedules) get a neutral gray, day 0-6 is Mon-Sun. */
.schedule-day-badge.day-any { background: var(--text-secondary); }
.schedule-day-badge.day-0,
.schedule-day-badge.day-1 { background: #3b6fd6; }
.schedule-day-badge.day-2 { background: #7c5cf0; }
.schedule-day-badge.day-3 { background: #f0a020; }
.schedule-day-badge.day-4 { background: #e2483d; }
.schedule-day-badge.day-5,
.schedule-day-badge.day-6 { background: #43a047; }

.schedule-day-times {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 10px;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
  font-size: var(--text-sm);
}

.schedule-time {
  display: inline-flex;
  align-items: center;
  gap: 3px;
}

.schedule-time__x {
  border: 0;
  background: none;
  cursor: pointer;
  color: var(--text-secondary);
  font-size: 1rem;
  line-height: 1;
  padding: 0 2px;
}
.schedule-time__x:hover { color: var(--danger); }

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

.btn-icon {
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text-secondary);
  cursor: pointer;
  font-size: 14px;
}

.btn-icon:hover {
  background: var(--bg-raised);
  color: var(--text-primary);
}

/* ── Forms ────────────────────────────────────────────────────────────── */

.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-6);
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--space-6);
  margin-bottom: var(--space-8);
}

.form-grid.full {
  grid-template-columns: 1fr;
}

/* .form-panel — extra visual weight for "add new X" action forms: raised
   background + accent left edge, so the primary action on a page reads as
   such while skimming. Layout comes from .form-grid / .field-inline. */
.form-panel,
.form-grid.form-panel {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-4) var(--space-4) 18px;
}

.form-panel.field-inline {
  flex-wrap: wrap;
}

/* Grouped form sections — real fieldsets. The legend carries an icon +
   semibold heading; a hairline separates consecutive sections. */
.form-section {
  border: 0;
  padding: 0;
  margin: 0;
  grid-column: 1 / -1;
  min-width: 0;
}

.form-section + .form-section {
  margin-top: var(--space-4);
  border-top: 1px solid var(--border);
  padding-top: var(--space-6);
}

.form-section__legend {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-display);
  font-size: 16px;
  font-weight: 600;
  color: var(--text-primary);
  padding: 0;
  margin-bottom: var(--space-3);
}

.form-section__legend .icon {
  color: var(--accent);
}

.form-section__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-6) 32px;
}

.form-grid.full .form-section__grid {
  grid-template-columns: 1fr;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}

.field.span-2 {
  grid-column: 1 / -1;
}

/* A .field is a stretch-aligned flex column — without this, a lone
   submit button inside one becomes a full-width slab. */
.field > .btn {
  align-self: flex-start;
}

/* A label-less field (just a submit button, no input above it) packs to
   the top by default, so it lines up with a sibling field's label
   instead of its input. Bottom-align it so the button sits level with
   the input next to it. */
.field:has(> .btn:only-child) {
  justify-content: flex-end;
}

/* Form labels: sentence case, near-primary weight/color — deliberately
   NOT micro-caps, so "type here" labels are distinct from data labels. */
.field label {
  font-size: 13px;
  font-weight: 500;
  color: var(--text-primary);
  line-height: 1.4;
}

.field .label-plain {
  font-weight: 400;
  color: var(--text-primary);
}

.field-hint {
  font-size: var(--text-sm);
  color: var(--text-tertiary);
  line-height: 1.5;
  margin-top: -2px;
}

/* Focused field: the label tints accent along with the input border. */
.field:focus-within > label {
  color: var(--accent);
}

/* Inputs — recessed wells. Border strictly stronger than card hairlines;
   hover shows reactivity; focus = accent border + soft halo. */
input[type="text"],
input[type="url"],
input[type="email"],
input[type="search"],
input[type="password"],
input[type="number"],
select,
textarea {
  background: var(--input-bg);
  border: 1px solid var(--input-border);
  color: var(--text-primary);
  padding: 8px 10px;
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: var(--text-base);
  box-shadow: inset 0 1px 2px color-mix(in srgb, var(--p-shadow) 40%, transparent);
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
  min-width: 0;
}

input[type="text"],
input[type="url"],
input[type="email"],
input[type="search"],
input[type="password"],
input[type="number"],
select {
  height: 38px;
}

input[type="url"],
input[type="password"] {
  font-family: var(--font-mono);
  font-size: 13px;
}

input:hover:not(:disabled),
select:hover:not(:disabled),
textarea:hover:not(:disabled) {
  border-color: var(--input-border-hover);
}

input[type="text"]:focus,
input[type="url"]:focus,
input[type="email"]:focus,
input[type="search"]:focus,
input[type="password"]:focus,
input[type="number"]:focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--input-border-focus);
  box-shadow: 0 0 0 3px var(--focus-ring),
    inset 0 1px 2px color-mix(in srgb, var(--p-shadow) 40%, transparent);
}

/* Filled state: a quiet "has content" cue when scanning long forms. */
input[type="text"]:not(:placeholder-shown),
input[type="url"]:not(:placeholder-shown),
input[type="email"]:not(:placeholder-shown),
input[type="search"]:not(:placeholder-shown),
input[type="password"]:not(:placeholder-shown) {
  border-color: var(--border-strong);
}

input[type="text"]:focus:not(:placeholder-shown),
input[type="url"]:focus:not(:placeholder-shown),
input[type="email"]:focus:not(:placeholder-shown),
input[type="search"]:focus:not(:placeholder-shown),
input[type="password"]:focus:not(:placeholder-shown) {
  border-color: var(--input-border-focus);
}

input:disabled,
select:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

input::placeholder,
textarea::placeholder {
  color: var(--input-placeholder);
  opacity: 1;
}

/* Native file inputs (documents.html's upload, the edit-form replace file,
   media_library_folder.html's no-JS fallback) — the browser's default
   "Choose File" button is unstyled and reads as broken next to every other
   Fluent-2 control on the same form. One rule here fixes it everywhere the
   element appears, rather than a per-page override. Excludes
   .is-upload-managed, which app.js visually hides once its own drag-and-drop
   zone takes over. */
input[type="file"] {
  /* No display/flex override on the host: Chrome lays out a file input's
     internal button + label in its own UA shadow tree, and that tree is
     what makes the whole control (not just the button) open the file
     dialog on click. Overriding display here previously broke that
     delegation, so clicking the "No file chosen" text did nothing. */
  height: 38px;
  box-sizing: border-box;
  line-height: 36px; /* height minus the 1px top/bottom border */
  padding: 0 12px 0 0;
  border: 1px solid var(--input-border);
  border-radius: var(--radius-md);
  background: var(--input-bg);
  color: var(--text-secondary);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  cursor: pointer;
}

input[type="file"]:hover:not(:disabled) {
  border-color: var(--input-border-hover);
}

input[type="file"]:focus-visible {
  outline: none;
  border-color: var(--input-border-focus);
  box-shadow: 0 0 0 3px var(--focus-ring);
}

input[type="file"]::file-selector-button {
  /* 3px margin on every side gives the button an equal inset gap from the
     host's border, matching Fluent 2's nested-control spacing — the
     previous 3px/10px split (padding vs. margin-right) is what made the
     gap look uneven on the right. */
  height: 30px;
  box-sizing: border-box;
  margin: 3px 12px 3px 3px;
  padding: 0 12px;
  line-height: 28px; /* button height minus its own 1px top/bottom border */
  border: 1px solid var(--input-border);
  border-radius: var(--radius-sm);
  background: var(--bg-raised);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: border-color 0.12s ease, background-color 0.12s ease;
}

input[type="file"]::file-selector-button:hover {
  border-color: var(--input-border-hover);
  background: color-mix(in srgb, var(--bg-raised) 80%, var(--bg-surface));
}

textarea {
  resize: vertical;
  min-height: 84px;
  line-height: 1.55;
}

select {
  appearance: none;
  /* Neutral chevron that clears contrast on every theme's input well —
     one glyph, zero per-theme overrides. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23898f9b' stroke-width='1.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  padding-right: 32px;
}

/* Error state — server-rendered on the wrapping .field. */
.field--error input,
.field--error select,
.field--error textarea {
  border-color: var(--danger);
  box-shadow: 0 0 0 3px var(--danger-ring);
}

.field--error > label {
  color: var(--danger);
}

.field-error-msg {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-sm);
  color: var(--danger);
}

/* Checkboxes — custom, token-driven. */
input[type="checkbox"] {
  appearance: none;
  width: 18px;
  height: 18px;
  margin: 0;
  flex-shrink: 0;
  border: 1px solid var(--input-border);
  border-radius: var(--radius-sm);
  background: var(--input-bg);
  cursor: pointer;
  display: inline-grid;
  place-content: center;
  transition: border-color 0.12s ease, background-color 0.12s ease;
}

input[type="checkbox"]::before {
  content: "";
  width: 10px;
  height: 10px;
  transform: scale(0);
  transition: transform 0.1s ease;
  background: var(--text-on-accent);
  clip-path: polygon(14% 44%, 0% 62%, 40% 100%, 100% 18%, 84% 4%, 38% 71%);
}

input[type="checkbox"]:hover {
  border-color: var(--input-border-hover);
}

input[type="checkbox"]:checked {
  background: var(--accent);
  border-color: var(--accent);
}

input[type="checkbox"]:checked::before {
  transform: scale(1);
}

input[type="checkbox"]:focus {
  outline: none;
  box-shadow: 0 0 0 3px var(--focus-ring);
}

.checkbox-field {
  flex-direction: row;
  align-items: flex-start;
  gap: 10px;
}

.checkbox-field input[type="checkbox"] {
  margin-top: 1px;
}

.field-inline {
  display: flex;
  align-items: center;
  gap: 10px;
}

.range-field {
  flex-direction: row;
  align-items: center;
  gap: 10px;
}

/* The value rides INSIDE the handle rather than sitting beside it, so the
   number and the thing it describes are one object.

   Positioned by percentage, never by measured pixels:
   `--range-pos` is a unitless 0-1 that app.js sets, and the calc below turns
   it into a left offset. That matters because every persona editor except
   "Add persona" lives inside a dialog, which has zero width until it opens —
   a JS implementation that measured offsetWidth would compute every handle's
   position as 0 and only correct itself if something later told it the
   dialog had opened. Percentages resolve against the element's real width
   whenever it acquires one, so there is nothing to re-run. */
.range-field__control {
  position: relative;
  flex: 1;
  display: flex;
  align-items: center;
  --range-thumb-size: 30px;
}

.range-field input[type="range"] {
  flex: 1;
  width: 100%;
  cursor: pointer;
  background: transparent;
  /* accent-color styled the native thumb and is mutually exclusive with
     styling it ourselves — setting one thumb property means taking over the
     track too, hence the four vendor blocks below. */
  -webkit-appearance: none;
  appearance: none;
}

.range-field input[type="range"]::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 999px;
  background: var(--input-border);
}

.range-field input[type="range"]::-moz-range-track {
  height: 4px;
  border-radius: 999px;
  background: var(--input-border);
}

.range-field input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: var(--range-thumb-size);
  height: var(--range-thumb-size);
  border-radius: 50%;
  background: var(--range-thumb);
  border: 1px solid var(--border-strong);
  /* Centres a 30px thumb on a 4px track. Chrome aligns the thumb to the top
     of the track box unless told otherwise; Firefox centres it already,
     which is why there is no equivalent below. */
  margin-top: calc((4px - var(--range-thumb-size)) / 2);
}

.range-field input[type="range"]::-moz-range-thumb {
  width: var(--range-thumb-size);
  height: var(--range-thumb-size);
  border-radius: 50%;
  background: var(--range-thumb);
  border: 1px solid var(--border-strong);
}

.range-field input[type="range"]:focus-visible::-webkit-slider-thumb {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.range-field input[type="range"]:focus-visible::-moz-range-thumb {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.range-field output {
  position: absolute;
  top: 50%;
  /* The handle's centre travels from half a thumb in to half a thumb from
     the end, so the usable span is (100% - thumb) and the offset is half a
     thumb. Same arithmetic the browser uses to place the thumb itself. */
  left: calc(
    var(--range-pos, 0) * (100% - var(--range-thumb-size)) +
      var(--range-thumb-size) / 2
  );
  transform: translate(-50%, -50%);
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  color: var(--range-thumb-text);
  /* Or dragging the handle by its own number would do nothing. */
  pointer-events: none;
}

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

.validate-row input {
  flex: 1;
}

.validate-result {
  margin-top: 4px;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.validate-result.ok {
  color: var(--success);
}

.validate-result.err {
  color: var(--danger);
}

/* ── Cards (single system) ────────────────────────────────────────────── */

.card-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-bottom: var(--space-8);
}

.card {
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--space-4) var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.card > summary.card__head {
  list-style: none;
  cursor: pointer;
  margin: calc(-1 * var(--space-4)) calc(-1 * var(--space-6));
  padding: var(--space-4) var(--space-6);
  border-radius: var(--radius-lg);
}

.card[open] > summary.card__head {
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}

.card > summary.card__head::-webkit-details-marker {
  display: none;
}

.card > summary.card__head:hover {
  background: color-mix(in srgb, var(--bg-raised) 55%, var(--bg-surface));
}

.card__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
}

.card__url-inline {
  flex: 1;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.card__chevron {
  flex-shrink: 0;
  color: var(--text-tertiary);
  display: inline-flex;
  transition: transform 0.15s ease;
}

.card[open] > summary.card__head .card__chevron {
  transform: rotate(90deg);
}

.card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-top: var(--space-3);
}

.card__title {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 600;
  color: var(--text-primary);
  margin: 0;
  text-transform: none;
  letter-spacing: 0;
}

/* Marks a default (template-seeded) persona — can't be edited or deleted.
   Native `title` attribute for the tooltip rather than info_tip: this is a
   status marker glanced at, not something worth a click/hover popup. */
.persona-lock {
  display: inline-flex;
  vertical-align: middle;
  margin-left: 4px;
  color: var(--text-tertiary);
}

.card__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  margin: 0;
}

.stat {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.stat__label {
  margin: 0;
}

.stat__value {
  font-size: var(--text-sm);
  color: var(--text-primary);
  margin: 0;
  text-transform: capitalize;
}

.time-pill {
  display: inline-block;
  background: var(--bg-inset);
  border-radius: var(--radius-sm);
  padding: 1px 6px;
  margin: 0 2px;
  white-space: nowrap;
}

.card__pills {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

.card__actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}

.card__actions form {
  display: inline;
  margin: 0;
}

/* The two footer clusters (generate on the left, edit/more on the right) —
   space-between pushes them apart, this keeps each cluster's own controls
   tight. */
.card__actions-group {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Matches .input-mini's rendered height (38px, inherited from the shared
   input[type="number"] rule) so Edit doesn't sit shorter than the count
   field beside it. */
.card__edit-btn {
  height: 38px;
}

/* .panel — generic titled content card (settings blocks, account rows,
   media folders...). Replaces the misnamed .persona-card. */
.panel {
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--space-4) var(--space-6);
  margin-bottom: var(--space-3);
}

.panel > h2 {
  margin: 0 0 var(--space-2);
  font-size: var(--text-md);
}

/* ── settings-grid ─────────────────────────────────────────────────────
   Lays out a row of same-width settings panels side by side instead of
   stacking every panel full-width — the account tab's panels are mostly
   two form fields and a button, and full-width was mostly empty space.
   auto-fit + minmax means a row still reads fine if the viewport is too
   narrow for N columns; it just wraps rather than squeezing. Panels lose
   their own bottom margin inside a grid (the grid's own `gap` replaces it)
   so two stacked grids don't end up with doubled spacing between them. */
.settings-grid {
  display: grid;
  align-items: stretch;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

/* Every panel in a row stretches to the row's tallest (align-items:
   stretch above), and becomes a column flexbox so its Save button lands on
   the same baseline across the row instead of floating right under
   whichever panel's fields happen to be shortest. The form is the flexible
   element, not the panel's title row, so a taller intro (e.g. Registered
   email's verified-badge line) still pushes the button down exactly as far
   as it needs to and no further. */
.settings-grid > .panel {
  margin-bottom: 0;
  display: flex;
  flex-direction: column;
}

.settings-grid > .panel > form.form-grid {
  flex: 1;
  display: flex;
  flex-direction: column;
  margin-bottom: 0;
}

.settings-grid > .panel > form.form-grid > .field:last-child {
  margin-top: auto;
}

.settings-grid--3 {
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}

.settings-grid--2 {
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
}

.panel .desc {
  margin-bottom: var(--space-2);
}

.panel .desc:last-child {
  margin-bottom: 0;
}

/* ── Pills, chips, tags ───────────────────────────────────────────────── */

.pill {
  display: inline-flex;
  align-items: center;
  padding: 2px 10px;
  border-radius: 999px;
  font-family: var(--font-body);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  white-space: nowrap;
  border: 1px solid transparent;
}

.pill__icon {
  display: inline-flex;
  vertical-align: -1px;
  margin-right: 4px;
}

.pill--muted {
  color: var(--text-secondary);
  border-color: var(--border-strong);
}
.pill--accent {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
}
.pill--success {
  color: var(--success);
  background: var(--success-soft);
  border-color: color-mix(in srgb, var(--success) 35%, transparent);
}
.pill--warning {
  color: var(--warning);
  background: var(--warning-soft);
  border-color: color-mix(in srgb, var(--warning) 35%, transparent);
}
.pill--danger {
  color: var(--danger);
  background: var(--danger-soft);
  border-color: color-mix(in srgb, var(--danger) 35%, transparent);
}
/* A deliberate, active, non-alarming state — the one the other four tones
   can't express. `muted` reads as switched off, `accent` is the brand color
   and so reads as "this is the normal one", and warning/danger already own
   orange and red as real severities. */
.pill--info {
  color: var(--info);
  background: var(--info-soft);
  border-color: color-mix(in srgb, var(--info) 35%, transparent);
}

/* Status chips — class names track content_blocks.status values. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
  white-space: nowrap;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  border: 1px solid transparent;
}

.chip.pending_review {
  color: var(--warning);
  background: var(--warning-soft);
  border-color: color-mix(in srgb, var(--warning) 35%, transparent);
}
.chip.approved {
  color: var(--success);
  background: var(--success-soft);
  border-color: color-mix(in srgb, var(--success) 35%, transparent);
}
.chip.rejected,
.chip.failed,
.chip.sensitive {
  color: var(--danger);
  background: var(--danger-soft);
  border-color: color-mix(in srgb, var(--danger) 35%, transparent);
}
.chip.posted,
.chip.archived {
  color: var(--text-secondary);
  border-color: var(--border-strong);
}

.chip.source {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}

/* Account posting cooldown, beside a ticket's actions. Muted rather than
   a warning tone on purpose: nothing is wrong and there is nothing to fix
   — the account posted recently and the wait clears itself. Rendering it
   in --warning would put an alert colour on the ordinary steady state of
   a busy workspace. */
.chip.cooldown {
  color: var(--text-secondary);
  border-color: var(--border-strong);
  text-transform: none;
  letter-spacing: 0;
}

/* Promo (document-source) items in the queue — soft accent, deliberately
   quieter than the solid .chip.source so the two read as related but
   distinct (same accent/accent-soft pairing as .pill--accent). */
.chip.promo {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
}

/* Compact inline number input — the Docs card's Generate-count field. */
.input-mini {
  width: 58px;
  padding: 3px 6px;
  font-size: 12px;
}

.tag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 9px;
  border-radius: var(--radius-sm);
  background: var(--bg-raised);
  border: 1px solid var(--border);
  color: var(--text-secondary);
  font-size: var(--text-xs);
  white-space: nowrap;
}

.tag--link {
  color: var(--accent);
  text-decoration: none;
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
}

.tag--link:hover {
  background: var(--accent);
  color: var(--accent-contrast);
}

/* ── Queue: tickets ───────────────────────────────────────────────────── */

.day-divider {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-8) 0 var(--space-3);
}

.day-divider:first-child {
  margin-top: 0;
}

.day-divider .day-label {
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

.day-divider .day-line {
  flex: 1;
  height: 1px;
  background: var(--border);
}

.ticket {
  position: relative;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  margin-bottom: var(--space-3);
  padding: var(--space-4) 18px;
  text-decoration: none;
  display: block;
  color: inherit;
  transition: border-color 0.12s ease, box-shadow 0.12s ease,
    transform 0.12s ease, opacity 0.18s ease;
}

.ticket:hover {
  border-color: var(--border-strong);
  box-shadow: var(--shadow-md);
  transform: translateY(-1px);
}

.ticket.htmx-swapping {
  opacity: 0;
  transform: scale(0.98);
}

.ticket-open {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  color: inherit;
  font: inherit;
  cursor: pointer;
}

.ticket-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  margin-bottom: 8px;
  padding-right: 76px;
}

.ticket-title {
  font-size: 13px;
  font-weight: 500;
  color: var(--ticket-title);
  margin-bottom: 6px;
}

.ticket-preview {
  position: relative;
  display: block;
  font-family: var(--font-content);
  font-size: var(--text-base);
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
}

.ticket-preview.is-error {
  color: var(--danger);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}

.ticket-preview::after {
  content: attr(data-full);
  position: absolute;
  left: 0;
  top: 100%;
  margin-top: 8px;
  width: min(480px, 90vw);
  background: var(--bg-overlay);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  padding: var(--space-3) 14px;
  font-size: var(--text-base);
  line-height: 1.55;
  white-space: pre-wrap;
  color: var(--text-primary);
  box-shadow: var(--shadow-md);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.12s ease, visibility 0s linear 0.38s;
  z-index: 20;
  pointer-events: none;
}

.ticket-preview:hover::after {
  opacity: 1;
  visibility: visible;
  transition-delay: 0.12s, 0s;
}

.ticket-actions {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.ticket-actions form {
  display: inline;
}

.ticket-time {
  position: absolute;
  top: 12px;
  right: 14px;
  display: inline-block;
  white-space: nowrap;
  padding: 2px 9px;
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.05em;
  border: 1px solid var(--accent);
  background: var(--accent);
  color: var(--accent-contrast);
}

.ticket-time--manual {
  background: none;
  border-color: var(--border-strong);
  color: var(--text-secondary);
  text-transform: uppercase;
}

/* Approve / reject stamps — the wire-desk signature action. Mono caps on
   purpose (these are "stamps", not regular buttons), soft-tint fills. */
.stamp-mini {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 12px;
  border-radius: var(--radius-md);
  border: 1px solid;
  background: none;
  cursor: pointer;
  transition: background-color 0.12s ease, color 0.12s ease;
}

.stamp-mini.approve {
  color: var(--success);
  border-color: color-mix(in srgb, var(--success) 50%, transparent);
  background: var(--success-soft);
}

.stamp-mini.approve:hover {
  background: var(--success);
  color: var(--bg-canvas);
}

.stamp-mini.reject {
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 50%, transparent);
  background: var(--danger-soft);
}

.stamp-mini.reject:hover {
  background: var(--danger);
  color: var(--bg-canvas);
}

.stamp-mini.retry {
  color: var(--text-secondary);
  border-color: var(--border-strong);
  background: none;
  padding: 4px 9px;
}

.stamp-mini.retry:hover {
  background: var(--bg-raised);
  color: var(--text-primary);
}

.stamp-row {
  display: flex;
  gap: 10px;
}

.stamp {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 18px;
  border-radius: var(--radius-md);
  border: 2px solid;
  background: none;
  cursor: pointer;
  transition: transform 0.08s ease, background-color 0.12s ease,
    color 0.12s ease;
}

.stamp:active {
  transform: scale(0.96);
}

.stamp-approve {
  color: var(--success);
  border-color: var(--success);
  background: var(--success-soft);
}

.stamp-approve:hover {
  background: var(--success);
  color: var(--bg-canvas);
}

.stamp-reject {
  color: var(--danger);
  border-color: var(--danger);
  background: var(--danger-soft);
}

.stamp-reject:hover {
  background: var(--danger);
  color: var(--bg-canvas);
}

.stamp-retry {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--accent-soft);
}

.stamp-retry:hover {
  background: var(--accent);
  color: var(--accent-contrast);
}

/* ── Retry strip / poll status ────────────────────────────────────────── */

.retry-strip {
  margin-bottom: var(--space-4);
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg-raised);
  font-size: var(--text-sm);
}

.retry-strip-row {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
}

.retry-strip-row form {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.retry-count-input {
  width: 64px;
}

input[type="number"].retry-count-input {
  height: 30px;
  padding: 3px 6px;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}

.retry-strip-label {
  color: var(--text-secondary);
}

.retry-strip-progress {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  color: var(--accent);
}

.retry-strip-progress-label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.retry-strip-summary {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  color: var(--text-secondary);
}

.retry-strip-stop {
  color: var(--danger);
}

.retry-strip-details {
  flex-basis: 100%;
  margin: var(--space-2) 0 0;
  padding-left: 18px;
  color: var(--text-secondary);
  font-size: var(--text-xs);
}

/* The status span's children (generate form, then — once a batch has
   run — the summary text and Dismiss) previously relied on incidental
   inline whitespace for spacing, which read as cramped/uneven. Explicit
   flex + gap instead. */
.poll-status {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.poll-status-progress,
.poll-status-summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
}

.poll-status-progress {
  color: var(--accent);
}

.poll-status-summary {
  color: var(--text-secondary);
}

.poll-status-summary.poll-status-error {
  color: var(--danger);
}

.poll-status-summary.poll-status-warning {
  color: var(--warning);
}

/* Fires its hx-trigger="load delay:…" once when swapped in, to
   auto-dismiss a clean success summary; renders nothing itself. */
.poll-status-auto-dismiss {
  display: none;
}

.progress-donut {
  --progress: 0%;
  display: inline-block;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  flex-shrink: 0;
  background: conic-gradient(var(--accent) var(--progress), var(--border) 0);
}

.progress-donut--indeterminate {
  background: conic-gradient(var(--accent) 0 30%, var(--border) 0);
  animation: progress-donut-spin 0.8s linear infinite;
}

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

/* ── Empty state / pagination / toolbar rows ──────────────────────────── */

.empty-state {
  color: var(--text-secondary);
  font-size: 13px;
  padding: 48px var(--space-4);
  text-align: center;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-lg);
}

.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  margin-top: var(--space-6);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}

.pagination__label {
  color: var(--text-secondary);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}

/* Small flex row between the tabs and the list: count + sort on the left,
   contextual bulk action on the right. */
.page-toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-4);
}

.meta-text {
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

.toolbar-link {
  margin-bottom: var(--space-3);
  font-size: var(--text-sm);
}

/* ── Detail / dispatch view ───────────────────────────────────────────── */

.dispatch {
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 28px 32px;
}

.dispatch-header {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 18px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--border);
}

.dispatch-source {
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.dispatch-source a {
  color: var(--text-secondary);
}

.wire-facts {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  background: var(--bg-inset);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-3) 14px;
  margin-bottom: 20px;
}

.wire-facts .label {
  display: block;
  margin-bottom: 6px;
}

.wire-facts ul {
  margin: 0 0 10px;
  padding-left: 18px;
}

.wire-facts .valence-row {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px dashed var(--border);
}

.valence-good {
  color: var(--success);
}

.valence-bad {
  color: var(--danger);
}

.dispatch-body {
  font-family: var(--font-content);
  font-size: 19px;
  line-height: 1.65;
  color: var(--text-primary);
  white-space: pre-wrap;
  margin-bottom: var(--space-6);
}

.dispatch-body--muted {
  opacity: 0.7;
}

.char-count {
  margin-top: 4px;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

.char-count.over {
  color: var(--danger);
}

.error-box {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: 13px;
  color: var(--danger);
  background: var(--danger-soft);
  border: 1px solid color-mix(in srgb, var(--danger) 35%, transparent);
  border-radius: var(--radius-md);
  padding: 12px 14px;
}

.info-box {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: 13px;
  color: var(--success);
  background: var(--success-soft);
  border: 1px solid color-mix(in srgb, var(--success) 35%, transparent);
  border-radius: var(--radius-md);
  padding: 12px 14px;
}

/* ── Guide box ────────────────────────────────────────────────────────────
   A read-this-before-you-do-the-thing block, given its own surface so it
   reads as guidance rather than as one more dimmed hint under a field. The
   New campaign form's document guidance is the first user.

   Deliberately NOT one of the tinted boxes below. Those set a *tinted*
   foreground (accent / success) on their own soft tint, which is tuned for
   a one-line status label; several paragraphs of body copy at that contrast
   is exactly the text people skip. This uses the neutral raised surface and
   the primary foreground — the same pairing as .info-tip__popup, so it is
   already proven legible in all eight themes rather than being a new colour
   relationship nobody has checked. The accent appears only on the icon and
   the left edge, where it marks the block without carrying any text. */
.guide-box {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  background: var(--bg-raised);
  border: 1px solid var(--border-strong);
  border-left-width: 3px;
  border-left-color: var(--accent);
  border-radius: var(--radius-md);
  padding: 14px 16px;
  color: var(--text-primary);
  font-size: var(--text-sm);
  line-height: 1.55;
}

.guide-box > .icon {
  color: var(--accent);
  flex: none;
  margin-top: 1px;
}

.guide-box__body > p {
  margin: 0;
}

.guide-box__body > p + p {
  margin-top: 8px;
}

/* The box's own title line, standing apart from the one-liners below it —
   a size step up and bold, same distinction a section legend already
   makes from its fields.

   Selector is `.guide-box__body > p.guide-box__heading`, not just
   `.guide-box__heading`, on purpose: `.guide-box__body > p { margin: 0; }`
   above has higher specificity than a single class (a combinator selector
   beats one), so a plain `.guide-box__heading` rule's margin-bottom was
   silently losing to that reset regardless of source order. */
.guide-box__body > p.guide-box__heading {
  font-size: var(--text-base);
  font-weight: 600;
  margin-bottom: 8px;
}

/* A row that leads with a small accent-colored arrow — the plain
   recommendation lines, not the caution/flag/link rows below them, which
   already carry their own icon or need none. currentColor on the icon
   inherits from this rule's own color, so it and the text can't drift out
   of sync with each other. */
.guide-box__point {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  color: var(--accent);
}

.guide-box__point > .icon {
  flex: none;
  margin-top: 2px;
}

.guide-box__point > span {
  color: var(--text-primary);
}

/* One recommendation per row, ruled off from the next by a full-width
   dashed line rather than by spacing alone — spacing says "these are
   related", a rule says "these are separate points", which is what a
   guide box of independent dos-and-don'ts actually is. Dashed rather than
   solid to match the box's own quieter register (the dashed rule already
   used for the hour grid and the fact-check valence row) — a solid line
   would read as heavier than four lines of advice warrant. */
.guide-box__list {
  display: flex;
  flex-direction: column;
}

.guide-box__item {
  padding: 10px 0;
  /* --border-strong, not --border. The divider sits on the box's own
     --bg-raised on both sides, and in the default Exo Teal dark theme
     --border (#242827) is actually darker than --bg-raised (#262a2a) —
     a line drawn darker than its own surface is invisible. --border-strong
     is what .guide-box's own outer edge already uses for the same reason. */
  border-top: 1px dashed var(--border-strong);
}

.guide-box__item:last-child {
  padding-bottom: 0;
}

.guide-box__item > p {
  margin: 0;
}

/* ── Explainer (collapsed prose) ──────────────────────────────────────────
   The ui.html explainer() macro. A slimmer sibling of .guide-box for prose
   that explains rather than warns: same neutral raised surface and primary
   foreground (proven legible in all eight themes), with the accent carried
   on a 3px top edge and the icon only, never on the text.

   Top edge rather than .guide-box's left edge on purpose — these stack
   under a section heading at full width, and a left rule there reads as a
   quotation rather than as a section marker.

   No open/close animation: a <details> height transition needs either JS
   measuring the panel or ::details-content, which is not in every browser
   this install serves, and neither is worth buying for a block whose whole
   job is to be out of the way. */
.explainer {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-top: 3px solid var(--accent);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  line-height: 1.55;
}

.explainer__summary {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 12px;
  cursor: pointer;
  color: var(--text-primary);
  list-style: none;
  user-select: none;
}

/* Safari still paints the disclosure triangle without this. */
.explainer__summary::-webkit-details-marker {
  display: none;
}

.explainer__summary > .icon {
  color: var(--accent);
  flex: none;
}

.explainer__label {
  font-weight: 500;
}

.explainer__chevron {
  margin-left: auto;
  display: inline-flex;
  color: var(--text-muted);
  transition: transform 140ms ease;
}

.explainer[open] .explainer__chevron {
  transform: rotate(180deg);
}

.explainer__summary:hover .explainer__chevron {
  color: var(--text-secondary);
}

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

.explainer__body {
  padding: 0 12px 12px 33px;
  color: var(--text-secondary);
}

.explainer__body > p {
  margin: 0;
}

.explainer__body > p + p {
  margin-top: 8px;
}

/* Neither an error nor a success — "here is something to act on when you
   want to". The halt banner uses it for the states that are an upsell or a
   wait rather than a fault (entitlements.Halt.severity), because rendering
   "add credit to post to X" in danger red is how a brand-new signup decides
   the product is broken before they have used it once. */
/* A single line inside a guide-box that has to be read before acting —
   the PDF export setting is the one piece of advice that changes what
   someone does *before* they come back to this form. Warning-toned rather
   than accent-toned so it separates from the guidance around it, and built
   from the theme's own warning tokens so it tracks all eight themes
   instead of pinning one colour. Deliberately not `explainer()`: that
   collapses, and a collapsed block is one most people never open. */
.guide-box__flag {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 10px 12px;
  border-radius: var(--radius-sm, 6px);
  background: var(--colorStatusWarningBackground1);
  border: 1px solid color-mix(in srgb, var(--p-warning) 35%, transparent);
  border-left: 3px solid var(--p-warning);
  color: var(--text-primary);
}

.guide-box__flag strong {
  color: var(--colorStatusWarningForeground1);
}

/* A paragraph inside a guide-box that has to be NOTICED but isn't a warning
   — the scope/privacy line ("nothing outside the brief is fair game; don't
   put anything in it you wouldn't want quoted") is a rule to read, not an
   alert to act on before proceeding, so it must not compete with
   .guide-box__flag's amber for the same attention. One step darker than the
   box's own --bg-raised (an inset panel, not a coloured one) is enough to
   separate it from the plain rows around it without borrowing a severity
   colour that isn't warranted. */
.guide-box__caution {
  padding: 10px 12px;
  border-radius: var(--radius-sm, 6px);
  background: var(--bg-inset);
  border: 1px solid var(--border);
}

.notice-box {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  color: var(--text-secondary);
  background: var(--accent-soft);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  border-radius: var(--radius-md);
  padding: 12px 14px;
}

.notice-box .icon {
  color: var(--accent);
}

.error-box .icon,
.info-box .icon {
  margin-top: 1px;
}

/* ── Tables ───────────────────────────────────────────────────────────── */

table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: var(--space-8);
}

/* A table whose columns need more room than the panel has scrolls
   horizontally instead of silently clipping trailing columns — which is
   what happened before this existed: `table { width: 100% }` keeps a table
   from growing past its container, and the only overflow-x: auto in this
   file lived inside a mobile media query, so a desktop table with enough
   columns lost its last one or two off the right edge with no scrollbar to
   reveal them. Wrap a table in this whenever its column count is data-driven
   or likely to grow (see admin/tenants.html). */
.table-scroll {
  overflow-x: auto;
}
.table-scroll table {
  min-width: max-content;
}

th {
  text-align: left;
  padding: var(--space-2) 10px;
  border-bottom: 1px solid var(--border-strong);
}

td {
  padding: 10px;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
  vertical-align: middle;
}

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

/* ── Info bar (Sources page header stats) ─────────────────────────────── */

.info-bar {
  display: flex;
  gap: 0;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  margin-bottom: 20px;
  overflow: hidden;
}

.info-bar-item {
  flex: 1;
  padding: 12px var(--space-4);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.info-bar-item:last-child {
  border-right: none;
}

.info-bar-label {
  display: block;
  margin-bottom: 4px;
}

.info-bar-value {
  font-family: var(--font-mono);
  font-size: var(--text-md);
  font-weight: 500;
  color: var(--text-primary);
}

/* For an info-bar item whose value is a prose string (an email, a date, "Not
   verified") rather than a short stat — the component's default mono/500/md
   styling is sized for numbers like plan limits and reads as shouting on a
   sentence-length value. */
.info-bar-value--plain {
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: 400;
}

.info-bar-item .desc {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: 1.35;
  color: var(--text-tertiary);
  margin-top: auto;
  padding-top: 0.35em;
}

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

.info-bar-sub {
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  margin-top: 2px;
}

/* ── Row menu (⋮ actions) ─────────────────────────────────────────────── */

.row-menu {
  position: relative;
  display: inline-block;
}

.row-menu > summary {
  list-style: none;
  cursor: pointer;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text-secondary);
  -webkit-user-select: none;
  user-select: none;
}

.row-menu > summary::-webkit-details-marker {
  display: none;
}

.row-menu > summary:hover,
.row-menu[open] > summary {
  background: var(--bg-raised);
  color: var(--text-primary);
}

.row-menu-list {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  background: var(--bg-overlay);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  padding: var(--space-1);
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 140px;
  z-index: 15;
  box-shadow: var(--shadow-md);
}

/* Right-aligned to its trigger by default, which is correct for a menu at
   the right edge of a card. Flipped left-aligned by app.js's positionPopup()
   on a narrow viewport, where a right-aligned menu wider than the space to
   its left runs off the screen. */
.row-menu--flip .row-menu-list {
  right: auto;
  left: 0;
}

.row-menu-list form {
  display: block;
}

.row-menu-list .btn {
  width: 100%;
  justify-content: flex-start;
  text-align: left;
}

/* ── Auth pages (login, register, forgot/reset password, notices) ──────── */
/* These are the only templates that don't extend base.html, so this is
   also the only chrome — header + footer — that brands them at all. */

/* The page is a flex column — header, then the card area taking the slack,
   then the footer. The chrome was fixed-positioned before, which meant the
   card's clearance was a pair of hand-tuned padding numbers that had to be
   re-tuned every time the logo or the button changed size, and that overlap
   the card outright on a short viewport. Nothing here is a magic number. */
body.auth-page {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: radial-gradient(
      ellipse 70% 50% at 50% -10%,
      color-mix(in srgb, var(--accent) 14%, transparent),
      transparent
    ),
    var(--bg-canvas);
}

.auth-header {
  display: grid;
  /* Equal side columns so the logo is centred on the page, not on the space
     the Back button happens to leave. */
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-6) var(--space-8);
}

.auth-header__side {
  display: flex;
  align-items: center;
}

.auth-header__side:last-child {
  justify-content: flex-end;
}

.auth-header__brand {
  display: inline-flex;
  align-items: center;
  justify-self: center;
  color: var(--text-primary);
  text-decoration: none;
}

.auth-header__logo {
  display: block;
  width: auto;
  height: 34px;
}

.auth-header__back {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* A bordered control rather than bare text: on a page with almost nothing
     else on it, an unbordered link reads as decoration. */
  background: var(--bg-raised);
  border: 1px solid var(--border);
  color: var(--text-secondary);
  font-size: var(--text-sm);
  font-weight: 500;
  cursor: pointer;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-md);
  transition: color 0.12s ease, border-color 0.12s ease;
}

.auth-header__back:hover {
  color: var(--text-primary);
  border-color: var(--text-secondary);
}

.auth-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-6) var(--space-8);
  font-size: var(--text-xs);
  color: var(--text-secondary);
  text-align: center;
}

.auth-footer__sep {
  opacity: 0.4;
}

.auth-footer a {
  color: var(--text-secondary);
  text-decoration: none;
}

.auth-footer a:hover {
  color: var(--text-primary);
  text-decoration: underline;
}

/* Takes the slack between header and footer and centres the card in it.
   `flex: 1` plus `min-height: 0` rather than a viewport height, so a card
   taller than the screen (register, on a phone) scrolls the page instead of
   being clipped. */
.login-shell {
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-8) var(--space-6);
}

.login-box {
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: var(--space-8) var(--space-8) var(--space-6);
  /* 320px was too tight once a form had four fields; min() keeps it from
     overflowing a narrow phone. */
  width: min(400px, 100%);
}

.login-box .brand {
  display: block;
  border: none;
  margin-bottom: var(--space-6);
  padding: 0;
  font-size: var(--text-xl);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text-primary);
  text-align: center;
}

/* An intro line under the title (forgot-password explains itself there).
   Pulled up against the title, which owns the gap below. */
.login-box .brand + .notice-message {
  margin-top: calc(var(--space-6) * -1 + var(--space-2));
  text-align: center;
}

/* The auth forms stack bare .field siblings — everywhere else in the app a
   field sits in a .form-grid, which supplies the gap. Without this the next
   field's label butts straight up against the previous field's input. */
.login-box form {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.login-error {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--danger);
  font-size: var(--text-sm);
}

.login-success {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--success);
  font-size: var(--text-sm);
}

.login-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-4);
  margin-top: var(--space-6);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* space-between puts a lone link hard against the left edge, which reads as
   a mistake next to a centred form. */
.login-links:has(> :only-child) {
  justify-content: center;
}

.login-links a {
  color: var(--text-secondary);
  text-decoration: none;
}

.login-links a:hover {
  color: var(--text-primary);
  text-decoration: underline;
}

.login-submit {
  /* On top of the form's own gap. */
  margin-top: var(--space-2);
}

.login-submit .btn {
  width: 100%;
}

.login-box--notice {
  text-align: center;
}

.notice-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  margin: 0 auto var(--space-4);
  border-radius: 50%;
}

.notice-icon--success {
  color: var(--success);
  background: color-mix(in srgb, var(--success) 14%, transparent);
}

.notice-icon--danger {
  color: var(--danger);
  background: color-mix(in srgb, var(--danger) 14%, transparent);
}

.notice-title {
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: var(--space-2);
}

.notice-message {
  margin: 0 0 var(--space-6);
  color: var(--text-secondary);
  font-size: var(--text-sm);
  line-height: 1.5;
}

.login-links--center {
  justify-content: center;
}

@media (max-width: 560px) {
  .auth-header {
    padding: var(--space-4);
  }

  .auth-header__logo {
    height: 26px;
  }

  /* The label is the first thing to go — the arrow still reads as Back, and
     at this width the button would otherwise crowd the logo. */
  .auth-header__back {
    padding: var(--space-2);
    font-size: 0;
    gap: 0;
  }

  .auth-footer {
    padding: var(--space-4);
  }

  .login-box {
    padding: var(--space-6) var(--space-4) var(--space-4);
  }
}

/* ── Personas ─────────────────────────────────────────────────────────── */

.slider-row {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin-bottom: 10px;
}

.slider-row strong {
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-weight: 500;
}

.slider-group-label {
  margin-bottom: 4px;
}

.trait-list {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin: 0;
  padding-left: 18px;
}

/* ── Dashboard: status cards ──────────────────────────────────────────── */

.status-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}

.status-card {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 20px 22px;
}

.status-card-body {
  min-width: 0;
}

/* Right-corner action slot (the /trends "Poll now" control): the button
   belongs with the card whose "last polled" line it updates. */
.status-card__action {
  margin-left: auto;
  align-self: flex-start;
  flex: none;
}

.status-dot {
  position: relative;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
  margin-top: 5px;
}

.status-dot::before {
  content: "";
  position: absolute;
  inset: -3px;
  border-radius: 50%;
  filter: blur(2px);
  opacity: 0.4;
  animation: status-dot-pulse 2.6s ease-in-out infinite;
}

.status-dot--green {
  background: var(--signal-ok);
  box-shadow: 0 0 2px 0px var(--signal-ok),
    0 0 5px 1px color-mix(in srgb, var(--signal-ok) 65%, transparent);
}

.status-dot--green::before {
  background: var(--signal-ok);
}

.status-dot--red {
  background: var(--signal-down);
  box-shadow: 0 0 2px 0px var(--signal-down),
    0 0 5px 1px color-mix(in srgb, var(--signal-down) 65%, transparent);
  animation: none;
}

.status-dot--red::before {
  background: var(--signal-down);
  animation: none;
  opacity: 0.25;
}

.status-dot--yellow {
  background: var(--signal-warn);
  box-shadow: 0 0 2px 0px var(--signal-warn),
    0 0 5px 1px color-mix(in srgb, var(--signal-warn) 65%, transparent);
  animation: none;
}

.status-dot--yellow::before {
  background: var(--signal-warn);
  animation: none;
  opacity: 0.25;
}

/* Staff/internal. Pulses like green rather than sitting still like yellow
   and red: the pulse is what separates "live and fine" from "stopped or
   needs a look", and an internal tenant is live and fine — it just isn't
   paying. Keeping the animation is the whole difference between reading as
   a state and reading as a flag. */
.status-dot--blue {
  background: var(--signal-staff);
  box-shadow: 0 0 2px 0px var(--signal-staff),
    0 0 5px 1px color-mix(in srgb, var(--signal-staff) 65%, transparent);
}

.status-dot--blue::before {
  background: var(--signal-staff);
}

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

.status-cell .status-dot {
  margin-top: 0;
}

@keyframes status-dot-pulse {
  0%,
  100% {
    transform: scale(0.9);
    opacity: 0.4;
  }
  50% {
    transform: scale(1.15);
    opacity: 0.05;
  }
}

.status-card-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: 2px;
}

.status-card-subtitle {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  margin-bottom: 8px;
}

.status-card-detail {
  font-size: 13px;
  color: var(--success);
}

.status-card-detail--error {
  color: var(--danger);
}

/* ── Image inbox / media ──────────────────────────────────────────────── */

.image-inbox-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--space-4);
}

.image-inbox-grid .panel {
  margin-bottom: 0;
  transition: opacity 0.18s ease, transform 0.12s ease;
}

.image-inbox-grid .panel.htmx-swapping {
  opacity: 0;
  transform: scale(0.98);
}

.image-inbox-thumb {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  background: var(--bg-inset);
}

/* Stands in for the <img> on an approved row whose media_file_id is
   unknown (predates the column) or whose library file has since been
   deleted — same box, same aspect ratio, so the grid doesn't reflow
   between real thumbnails and this. */
.image-inbox-thumb--gone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  color: var(--text-tertiary);
  font-size: var(--text-sm);
}

.image-meta {
  display: flex;
  justify-content: space-between;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin-top: var(--space-2);
}

/* Retention countdown on a pending inbox image. Uses the existing --danger
   tokens rather than a literal red so it stays correct in both themes, and
   --danger-soft rather than a solid fill: this is a deadline, not a failure,
   and a full-strength red banner on every unreviewed image would train people
   to ignore the colour everywhere else it appears. */
.image-expiry {
  margin-top: var(--space-2);
  padding: 4px var(--space-2);
  border-radius: var(--radius-sm);
  border: 1px solid color-mix(in srgb, var(--danger) 35%, transparent);
  background: var(--danger-soft);
  color: var(--danger);
  font-size: var(--text-xs);
  font-weight: 500;
  line-height: 1.3;
}

.image-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-sm);
  margin: 4px 0;
}

.image-actions {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

/* Button flush right, hint truly centered on the box regardless of the
   button's own width — a `justify-content: space-between` would center the
   hint between the button and the input (invisible, but still a flex
   item), not on the box itself. */
.upload-row {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
}

.upload-row .dropzone-hint {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
}

/* ── Media Library: OS-style folder grid ─────────────────────────────────
   Replaced the old "every folder expanded as its own tray" layout
   (2026-08-12) — that was unmanageable past a handful of folders. Each
   tile is a plain link to the folder's own page; the file grid below
   belongs to that page now, not the tile. */
.folder-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--space-3);
  margin-top: var(--space-4);
}

.folder-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: var(--space-4) var(--space-2);
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  text-align: center;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  transition: border-color 0.15s ease, transform 0.1s ease;
}

.folder-card:hover,
.folder-card:focus-visible {
  border-color: var(--border-strong);
  transform: translateY(-1px);
  outline: none;
}

.folder-card__icon {
  color: var(--text-tertiary);
  margin-bottom: 2px;
}

.folder-card__name {
  width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
  font-size: var(--text-sm);
}

.folder-card__stats {
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

.folder-card__campaigns {
  width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 3px;
}

.folder-card__campaigns--unassigned {
  font-style: italic;
}

/* A <button>, not an <a> — it opens a dialog rather than navigating —
   but must read as one more tile in the grid. */
.folder-card--new {
  background: transparent;
  border-style: dashed;
  font: inherit;
}
.folder-card--new:hover,
.folder-card--new:focus-visible {
  background: var(--bg-surface);
}

.media-file-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
  gap: var(--space-2);
}

.media-file-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: 100%;
  transition: opacity 0.18s ease, transform 0.12s ease;
}

.media-file-card.htmx-swapping {
  opacity: 0;
  transform: scale(0.98);
}

.media-file-thumb-wrap {
  position: relative;
  width: 100%;
}

.media-file-thumb {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  background: var(--bg-inset);
}

.media-file-ext {
  position: absolute;
  top: 4px;
  right: 4px;
  padding: 1px 5px;
  border-radius: var(--radius-sm);
  background: var(--bg-overlay);
  border: 1px solid var(--border);
  color: var(--text-secondary);
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.04em;
  line-height: 1.4;
}

.media-file-name {
  width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 10px;
  color: var(--text-secondary);
  text-align: center;
}

.upload-row input[type="file"] {
  flex: 1;
  min-width: 0;
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

/* Added by app.js once the "Choose files" + "Upload" merge takes over —
   the input stays in the DOM and still submits with the form, it's just
   no longer its own visible control. Visually-hidden rather than
   display:none so it stays focusable/clickable for the JS that drives it. */
.upload-row input[type="file"].is-upload-managed {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ── Dialogs ──────────────────────────────────────────────────────────── */

.dialog {
  background: var(--bg-surface);
  color: var(--text-primary);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: 24px 28px 28px;
  width: min(720px, 92vw);
  max-height: 86vh;
  overflow-y: auto;
}

.dialog::backdrop {
  background: rgba(0, 0, 0, 0.55);
}

.dialog__title {
  font-size: 17px;
  margin: 0 0 var(--space-4);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--border);
}

.dialog__close-row {
  display: flex;
  justify-content: flex-end;
  margin: -8px -12px 0 0;
}

.dialog__close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  background: none;
  border: none;
  border-radius: 50%;
  color: var(--text-secondary);
  cursor: pointer;
  padding: 0;
}

.dialog__close:hover {
  color: var(--text-primary);
  background: var(--bg-raised);
}

/* Section heading separating independent forms inside one dialog. */
.dialog-section {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-primary);
  margin: var(--space-6) 0 var(--space-2);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}

.dialog-section:first-of-type {
  margin-top: 0;
  padding-top: 0;
  border-top: none;
}

.dialog-section .icon {
  color: var(--accent);
}

.dialog .form-grid {
  background: none;
  border: none;
  box-shadow: none;
  padding: 0;
  margin-bottom: var(--space-4);
}

/* Pinned to the bottom of the scrolling .dialog itself (not the viewport),
   so Save/Cancel stay reachable on a long form without hunting for the
   scrollbar's end — the negative margin/bottom cancels .dialog's own
   24px/28px/28px padding so the bar sits flush, then the padding is put
   back on the bar itself. grid-column spans it full width even when the
   markup gives it no span-2 of its own (a .dialog__footer is always meant
   to run edge to edge, whatever else shares its grid track). */
/* Footer with something on the left as well as the buttons on the right —
   the weekly planner's "repeat" checkbox. A modifier rather than changing
   .dialog__footer itself, because every other dialog's footer is buttons
   only and flex-end is right for those. */
.dialog__footer--split {
  justify-content: space-between;
  align-items: center;
}

/* The checkbox sits in a footer, not in a form grid, so it must not inherit
   the block margins a .field carries between rows. */
.planner-recurring {
  margin: 0;
  min-width: 0;
}

.dialog__footer {
  display: flex;
  /* ⚠️ Declared, not left to the default, because five of the eight footers
     in the app carry BOTH classes on one element (`class="field
     dialog__footer"`) and `.field` sets flex-direction: column. With no
     direction of its own this rule lost silently and those five stacked
     their buttons vertically — Cancel above Save, full width. It was easy
     to miss at the old 8px gap and became obvious the moment the gap grew.
     A footer's direction is this rule's business; never rely on whatever
     else is on the element. */
  flex-direction: row;
  gap: var(--space-4);
  justify-content: flex-end;
  align-items: center;
  grid-column: 1 / -1;
  position: sticky;
  bottom: -28px;
  /* ⚠️ The three 28px values are one number, not three: .dialog's own
     horizontal padding. The negative margins cancel it so the footer's
     background and top border bleed edge to edge, and the padding puts it
     back so the footer's contents line up with the body above. Widening the
     padding alone (tried 40px, 2026-08-30) breaks that alignment — if
     .dialog's padding changes, all four move together. */
  margin: var(--space-4) -28px -28px;
  padding: var(--space-3) 28px 24px;
  background: var(--bg-surface);
  border-top: 1px solid var(--border);
}

/* Short labels ("Done", "Save") otherwise render as near-squares, which
   reads as an icon button rather than the dialog's primary action. A floor,
   not a fixed width, so a longer label still grows. */
.dialog__footer .btn {
  min-width: 132px;
}

/* ── Keep-order list (Settings → Profiles, §3.8b) ─────────────────────── */

.keep-order {
  list-style: decimal;
  margin: var(--space-3) 0 var(--space-2);
  padding-left: 2.2em;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.keep-order__row {
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--border);
}

.keep-order__row:last-child {
  border-bottom: none;
}

/* The row content, not the <li> itself — the marker stays outside so the
   numbering reads as the priority it is. */
.keep-order__name {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

/* Green padlock on the rows the plan covers. Deliberately a *positive*
   marker rather than relying on the "would be suspended" chip alone:
   working out whether your own profile is safe by counting rows against a
   cap is work, and this is the screen where getting it wrong costs someone
   their live @-Account. Same shape as .persona-lock — inline, native title,
   glanceable — but coloured, because here it means "kept" rather than
   "read-only". */
.keep-order__kept {
  display: inline-flex;
  vertical-align: middle;
  color: var(--success);
}

.keep-order__row .btn-row {
  float: right;
}

/* Past the plan's cap. Deliberately not .error-box red: nothing is wrong
   yet and nothing is lost when it happens — this is a forecast of a
   reversible state, so it reads as muted rather than as a fault. */
.keep-order__row--over .keep-order__name {
  color: var(--text-secondary);
}

/* ── Timeline (dashboard schedule) ────────────────────────────────────── */

.timeline {
  /* The "now" marker's own colour, deliberately NOT --danger, which it used
     to borrow. Danger carries "something is wrong" on every other screen, so
     a red line made a healthy schedule read as a fault.

     ⚠️ A bright NEUTRAL rather than a hue, and the reason is the dot palette
     two rules down: source_color() blankets the hue wheel with ten muted
     mid-tones, so any new hue reads as an eleventh campaign. Brightness is
     the axis left to differentiate on — this is the Google-Calendar
     convention inverted, since that grid is neutral and picks red as the one
     saturated thing on it, while this grid is already all colour and the
     loudest available signal is the least saturated one.

     Resolving to --text means every theme, light or dark, gets a marker at
     full contrast against its own ground with no per-theme entry to add and
     no contrast audit when a ninth theme lands. Weight and the square caps
     separate it from the 1px --border gridlines, not hue. */
  --timeline-now: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-surface);
  box-shadow: var(--shadow-sm);
  padding: var(--space-2) var(--space-4) var(--space-4);
}

.timeline-row {
  display: flex;
  align-items: center;
  min-height: 34px;
}

.timeline-header-row {
  min-height: 24px;
  margin-bottom: 4px;
}

.timeline-label {
  width: 160px;
  flex-shrink: 0;
  padding-right: var(--space-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.source-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  max-width: 100%;
  padding: 2px 9px 2px 3px;
  border-radius: 999px;
  font-size: var(--text-xs);
  color: var(--accent-contrast);
  font-weight: 600;
  white-space: nowrap;
}

.source-pill__name {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The pill's background is a per-campaign hue from the dashboard palette,
   not a theme token, so the destination logo can't just inherit currentColor
   and stay legible. It sits in its own disc painted from theme tokens
   instead: solid surface fill, a stroke to separate it from whichever hue
   the pill happens to be, and the primary text color for the mark itself —
   high contrast in every theme, light or dark. */
.source-pill__logo {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  color: var(--text);
}

.timeline-track {
  position: relative;
  flex: 1;
  height: 20px;
  background-image: repeating-linear-gradient(
      to right,
      var(--border-strong) 0,
      var(--border-strong) 1px,
      transparent 1px,
      transparent calc(100% / 8)
    ),
    repeating-linear-gradient(
      to right,
      var(--border) 0,
      var(--border) 1px,
      transparent 1px,
      transparent calc(100% / 24)
    );
}

.timeline-axis {
  height: 16px;
  background-image: none;
}

.axis-label {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-tertiary);
  white-space: nowrap;
}

.axis-now-line {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--timeline-now);
  opacity: 0.8;
  transform: translateX(-50%);
}

/* Each row draws its own line segment at the same left offset (same
   technique .timeline-dot uses) rather than one element spanning the
   whole chart. Extend each segment to its row's full height, not just
   its track's — the track is vertically centered within the taller
   row, so without this the line has a gap at every row boundary and
   reads as dashed instead of one continuous line. The header row's
   segment also has to bridge its own 4px margin-bottom (see
   .timeline-header-row) down to the first source row, hence the
   larger bottom offset here than the top one. */
.timeline-axis .axis-now-line {
  top: -4px;
  bottom: -8px;
}

.timeline-row:not(.timeline-header-row) .axis-now-line {
  top: -7px;
  bottom: -7px;
}

/* Square end-caps belong only at the very top and very bottom of the
   whole chart — the header row's segment (top) and the last source
   row's segment (bottom) — never on every row, or they'd repeat down
   the chart. */
.timeline-header-row .axis-now-line::before {
  content: "";
  position: absolute;
  top: -6px;
  left: 50%;
  transform: translateX(-50%);
  width: 6px;
  height: 6px;
  background: var(--timeline-now);
}

.timeline-row:last-child .axis-now-line::after {
  content: "";
  position: absolute;
  bottom: -6px;
  left: 50%;
  transform: translateX(-50%);
  width: 6px;
  height: 6px;
  background: var(--timeline-now);
}

.timeline-legend {
  margin: 0 0 var(--space-2);
  font-size: var(--text-xs);
  color: var(--text-muted);
}

/* Two dot states, because the axis is one day and the schedule is a week
   (see web/routers/dashboard.py). The campaign's own hue means the slot
   fires TODAY — whether or not it has already gone out, since the now-line
   already draws that boundary across the whole chart — and every other day
   of the week is a neutral grey that can't be mistaken for one of the ten
   campaign palette hues. The hue arrives as an inline --dot-color custom
   property so one class set covers every campaign. */
.timeline-dot {
  position: absolute;
  top: 50%;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  border: 1px solid rgba(0, 0, 0, 0.3);
  cursor: default;
}

.timeline-dot--today {
  background: var(--dot-color);
}

.timeline-dot--other {
  background: var(--text-muted);
  border-color: transparent;
  opacity: 0.45;
}

/* ── Trends ───────────────────────────────────────────────────────────── */
/* Chart.js (responsive:true) manages the canvas's own inline width/height
   and sizes off its immediate parent — the wrapper below is the actual
   size anchor; the canvas just fills it (see trends-charts.js). */
.trend-spark-wrap {
  position: relative;
  width: 100%;
  /* No max-width: the old 480px cap was a leftover from when this was a
     compact "sparkline" widget — .card/.card__body themselves are
     already full-width with no cap, so it was just wasting space now
     that the chart carries axes/legend like a real chart. */
  /* taller than the old 80px to leave room for the date axis + interest
     ticks added alongside the peak-dot caption below */
  height: 108px;
}

.trend-spark {
  width: 100%;
  height: 100%;
  display: block;
}

.trend-spark-caption {
  font-size: var(--text-xs);
  color: var(--text-tertiary);
  margin: var(--space-2) 0 0;
}

/* Fixed to match the equally fixed (non-themed) point colors in
   trends-charts.js — these mark specific points, not series identity. */
.trend-spark-caption__marker--peak {
  color: #ff7a1a;
}

.trend-spark-caption__marker--latest {
  color: #3b82f6;
}

/* ── Utilities ────────────────────────────────────────────────────────── */

/* Screen-reader-only: removed from flow (position: absolute) rather than
   just clipped, so it doesn't eat a flex gap next to a platform_icon —
   see platform_name() in _components/ui.html. */
.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;
}

/* A standalone line of guidance that is not a panel, a field hint or a
   status box. Unlike .desc it keeps the primary foreground: it is the only
   thing in its own space, so a dimmed one reads as decoration. */
/* The sidebar's "Top up X" link lands here; without a scroll margin the
   panel's top edge sits flush against the viewport. */
#top-up {
  scroll-margin-top: var(--space-4);
}

.note-line {
  color: var(--text-primary);
  font-size: var(--text-sm);
  line-height: 1.55;
  margin: 0;
}

.mb-4 {
  margin-bottom: var(--space-4);
}
.mt-4 {
  margin-top: var(--space-4);
}
.ml-2 {
  margin-left: var(--space-2);
}
.card-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.btn-row {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.actions-end {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
}
.form-inline {
  display: inline;
}

.controls-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: var(--space-6);
}
.cred-set {
  color: var(--text-secondary);
  font-weight: normal;
}
.cred-unset {
  color: var(--danger);
  font-weight: normal;
}

/* ── Responsive ───────────────────────────────────────────────────────── */

@media (max-width: 880px) {
  .shell {
    flex-direction: column;
  }

  .sidebar {
    width: 100%;
    height: auto;
    position: static;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    border-right: none;
    border-bottom: 1px solid var(--border);
  }

  .brand {
    width: 100%;
    border-bottom: none;
    padding-bottom: 0;
    margin-bottom: var(--space-1);
  }

  .nav-section {
    flex-direction: row;
    flex-wrap: wrap;
    margin-bottom: 0;
    gap: 2px;
  }

  .nav-section-label {
    display: none;
  }

  .nav-link {
    padding: 6px 10px;
  }

  .sidebar-spacer {
    display: none;
  }

  .profile-switcher,
  .theme-switcher {
    margin-bottom: 0;
    flex: 1 1 140px;
  }

  .theme-switcher-label {
    display: none;
  }

  .new-profile-link {
    margin-bottom: 0;
  }

  .logout-form {
    flex: 0 0 auto;
  }

  .app-version {
    display: none;
  }

  .content {
    padding: var(--space-4);
  }

  .form-grid,
  .form-section__grid {
    grid-template-columns: 1fr;
  }

  .field.span-2 {
    grid-column: auto;
  }

  .plan-bento {
    grid-template-columns: 1fr;
  }

  .info-bar {
    flex-direction: column;
  }

  .info-bar-item {
    border-right: none;
    border-bottom: 1px solid var(--border);
  }

  .info-bar-item:last-child {
    border-bottom: none;
  }

  .timeline-label {
    width: 90px;
  }

  .ticket-meta {
    padding-right: 0;
    margin-top: 18px;
  }

  table {
    display: block;
    overflow-x: auto;
  }
}

/* ── Onboarding checklist (SaaS plan Phase 7) ─────────────────────────── */

.checklist {
  list-style: none;
  margin: var(--space-4) 0 0;
  padding: 0;
}

.checklist-item {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-top: 1px solid var(--border);
}

.checklist-item:first-child {
  border-top: none;
}

.checklist-mark {
  display: flex;
  color: var(--text-secondary);
  margin-top: 1px;
}

.checklist-item--done .checklist-mark {
  color: var(--success);
}

.checklist-body {
  flex: 1;
  min-width: 0;
}

.checklist-title {
  font-weight: 500;
}

/* Done steps recede rather than disappear — the list is also a summary of
   what setup involved, which is worth keeping legible after the fact. */
.checklist-item--done .checklist-title {
  color: var(--text-secondary);
  text-decoration: line-through;
}

.steps {
  margin: var(--space-4) 0 0;
  padding-left: var(--space-6);
  color: var(--text-secondary);
  font-size: 13px;
}

.steps li {
  margin-bottom: var(--space-2);
}

/* A numbered walkthrough with a connecting line between steps — for
   explaining a flow ("how X works"), not a to-do list (that's .checklist).
   The line is one pseudo-element per item spanning from its own circle
   down to the next one, so it naturally stops after the last item rather
   than needing a wrapper element sized to the list. */
.step-flow {
  list-style: none;
  margin: var(--space-4) 0 0;
  padding: 0;
}

/* --space-5 is not in the scale, so this used to resolve to nothing and
   the steps sat flush against each other. --space-6 (24px) is the real
   token; the connector line below is drawn against this padding. */
.step-flow-item {
  position: relative;
  display: flex;
  gap: var(--space-4);
  padding-bottom: var(--space-6);
}

.step-flow-item:last-child {
  padding-bottom: 0;
}

.step-flow-item:not(:last-child)::before {
  content: "";
  position: absolute;
  left: 15px;
  top: 32px;
  bottom: 4px;
  width: 2px;
  background: var(--border);
}

.step-flow-num {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-contrast);
  font-size: 13px;
  font-weight: 600;
  z-index: 1;
}

.step-flow-body {
  padding-top: 4px;
}

.step-flow-title {
  font-weight: 600;
  margin-bottom: var(--space-1);
}

.step-flow-body .desc {
  margin: 0;
}

/* ── Reduced motion ───────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ── Health dashboard (SaaS plan Phase 9f) ─────────────────────────────────
   Superadmin-only, /admin/health.

   The colour rule here is the same one the module and the template both
   carry, expressed in CSS: `--unknown` is a *distinct* state from `--ok`
   and must never be styled to resemble it. A metric that could not be
   measured is drawn muted and dashed, so an unreachable Redis reads as
   "no answer" rather than as a healthy zero. `--plain` is for metrics with
   no threshold configured — no opinion, so no signal colour, because a
   green tile is an opinion. */
.health-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--space-3);
  margin: var(--space-4) 0;
}

.health-tile {
  position: relative;
  overflow: hidden;
  padding: 14px 16px 10px;
  border: 1px solid var(--border);
  border-left: 3px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-surface);
}

.health-tile__label {
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.health-tile__value {
  margin-top: 4px;
  font-size: 22px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
}

.health-tile__sub {
  margin-top: 2px;
  font-size: 12px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* Sparkline. currentColor, so it inherits the tile's tone — which means a
   line does not need its own colour rule per state. Height is fixed and
   the viewBox does not vary between tiles: two adjacent sparklines on
   different vertical scales is the one way a sparkline actively lies. */
.health-spark {
  display: block;
  width: 100%;
  height: 28px;
  margin-top: 8px;
  opacity: 0.55;
}

.health-tile--ok { border-left-color: var(--signal-ok); color: inherit; }
.health-tile--ok .health-spark { color: var(--signal-ok); }

.health-tile--warn {
  border-left-color: var(--warning);
  background: color-mix(in srgb, var(--warning) 6%, var(--bg-surface));
}
.health-tile--warn .health-spark { color: var(--warning); }

.health-tile--critical {
  border-left-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 8%, var(--bg-surface));
}
.health-tile--critical .health-spark { color: var(--danger); }

/* Deliberately dashed and desaturated — it must not be mistakable for ok. */
.health-tile--unknown {
  border-left-style: dashed;
  border-left-color: var(--text-muted);
}
.health-tile--unknown .health-tile__value { color: var(--text-muted); }

.health-tile--plain { border-left-color: var(--border); }
.health-tile--plain .health-spark { color: var(--accent); }

.panel--alert {
  border-color: color-mix(in srgb, var(--danger) 40%, var(--border));
  background: color-mix(in srgb, var(--danger) 4%, var(--bg-surface));
}

.desc--warn { color: var(--warning); }

/* Label/value pairs that are worth showing but not worth a tile. */
.kv-grid {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 6px var(--space-4);
  margin: var(--space-4) 0 0;
  font-size: 13px;
}

.kv-grid dt { color: var(--text-muted); }
.kv-grid dd { margin: 0; font-variant-numeric: tabular-nums; }

/* ── Storage gauge ────────────────────────────────────────────────────────
   One component, four placements (Media Library, Dashboard, Settings →
   Plan & usage, admin tenant detail). The --level modifier carries the
   same vocabulary as the 9f health tiles — plain / ok / warn / critical —
   so an unmetered tenant renders neutral rather than green: no threshold
   means no opinion, and a green bar is an opinion. */
.storage-gauge {
  --gauge-color: var(--text-tertiary);
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  margin-bottom: 20px;
}

.storage-gauge--ok { --gauge-color: var(--success); }
.storage-gauge--warn { --gauge-color: var(--warning); }
.storage-gauge--critical { --gauge-color: var(--danger); }

.storage-gauge--compact {
  padding: 10px var(--space-4);
  margin-bottom: 0;
  border: none;
  background: none;
}

.storage-gauge__head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
}

.storage-gauge__used {
  font-family: var(--font-mono);
  font-size: var(--text-md);
  font-weight: 500;
  color: var(--text-primary);
}

.storage-gauge__limit {
  font-size: var(--text-xs);
  color: var(--text-tertiary);
}

.storage-gauge__pct {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--gauge-color);
  font-weight: 600;
}

.storage-gauge__track {
  margin-top: 8px;
  height: 6px;
  border-radius: 3px;
  background: var(--border);
  overflow: hidden;
}

.storage-gauge__fill {
  height: 100%;
  background: var(--gauge-color);
  border-radius: 3px;
  transition: width 240ms ease;
}

.storage-gauge__breakdown {
  display: flex;
  flex-wrap: wrap;
  gap: 4px var(--space-4);
  margin-top: 10px;
}

.storage-gauge__cat {
  display: flex;
  gap: 6px;
  align-items: baseline;
  font-size: var(--text-xs);
}

.storage-gauge__cat-label { color: var(--text-tertiary); }

.storage-gauge__cat-value {
  font-family: var(--font-mono);
  color: var(--text-primary);
}

.storage-gauge__note {
  margin: 10px 0 0;
  font-size: var(--text-xs);
  color: var(--text-tertiary);
}

.storage-gauge__note--critical { color: var(--danger); }

/* ── Media lightbox ───────────────────────────────────────────────────────
   Full-size viewer for a media thumbnail. Sized against the viewport, not
   the image, so a 4000px asset and a 200px one both open sensibly and
   neither can push the page wider than the screen. */
.lightbox {
  border: none;
  background: transparent;
  padding: 0;
  max-width: 96vw;
  max-height: 96vh;
  overflow: visible;
}

.lightbox::backdrop {
  background: rgb(0 0 0 / 78%);
  backdrop-filter: blur(2px);
}

.lightbox__stage {
  display: flex;
  align-items: center;
  justify-content: center;
}

.lightbox__media {
  display: block;
  max-width: 92vw;
  max-height: 84vh;
  width: auto;
  height: auto;
  border-radius: var(--radius-lg);
  box-shadow: 0 24px 64px rgb(0 0 0 / 50%);
  background: var(--bg-surface);
}

.lightbox__caption {
  margin: 12px 0 0;
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: #e6e6e6;
  word-break: break-all;
}

.lightbox__close {
  position: absolute;
  top: -34px;
  right: 0;
  border: none;
  background: transparent;
  color: #fff;
  font-size: 28px;
  line-height: 1;
  cursor: pointer;
  padding: 0 6px;
  opacity: 0.75;
}

.lightbox__close:hover { opacity: 1; }

/* Only cue "this opens bigger" where the lightbox is actually wired up. */
[data-lightbox] { cursor: zoom-in; }

/* ── Password visibility toggle ───────────────────────────────────────────
   Login / registration password fields. The input keeps its normal width;
   right padding makes room for the button so typed text never runs under
   the icon. */
.password-field {
  position: relative;
}

.password-field input[type="password"],
.password-field input[type="text"] {
  padding-right: 36px;
  width: 100%;
}

.password-toggle {
  position: absolute;
  top: 0;
  right: 0;
  height: 38px;
  width: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  color: var(--text-tertiary);
  cursor: pointer;
}

.password-toggle:hover { color: var(--text-primary); }

/* ── Legal document modal ─────────────────────────────────────────────────
   Privacy Policy / Terms of Service, opened in-page instead of a new tab
   (registration form). A document reads better tall and narrower than the
   lightbox's image frame, so this is its own size rather than a shared
   class. */
.legal-modal {
  border: none;
  border-radius: var(--radius-lg);
  padding: 0;
  width: min(720px, 92vw);
  height: 86vh;
  overflow: hidden;
  background: var(--bg-surface);
  box-shadow: 0 24px 64px rgb(0 0 0 / 50%);
}

.legal-modal::backdrop {
  background: rgb(0 0 0 / 78%);
  backdrop-filter: blur(2px);
}

.legal-modal__bar {
  display: flex;
  justify-content: flex-end;
  padding: 8px;
  border-bottom: 1px solid var(--border);
}

.legal-modal__close {
  border: none;
  background: transparent;
  color: var(--text-muted);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  padding: 0 6px;
}

.legal-modal__close:hover { color: var(--text-primary); }

.legal-modal__frame {
  display: block;
  width: 100%;
  height: calc(100% - 41px);
  border: none;
}

/* ── Drag-and-drop upload ─────────────────────────────────────────────────
   Enhancement only — the file input and its button work untouched with JS
   off, and every check that matters (extension, size, quota) is on the
   server. Always shown as a dashed box (2026-08-13, was invisible until a
   drag was already in progress) — the whole point of a dropzone is that
   you can see it's one before you start dragging. */
[data-dropzone] {
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-3);
  background: var(--bg-inset);
  transition: background 140ms ease, border-color 140ms ease;
}

[data-dropzone].is-dragover {
  border-color: var(--accent);
  border-style: solid;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

[data-dropzone] .dropzone-hint {
  font-size: var(--text-xs);
  color: var(--text-tertiary);
}

[data-dropzone].is-dragover .dropzone-hint { color: var(--accent); }

/* ── Weekly schedule planner ──────────────────────────────────────────────
   Hourly rows. It was half-hourly until 2026-08-04, when the time popup made
   exact minutes selectable — at that point a 30-minute grid stopped meaning
   anything, since 03:33 sits on no boundary at all. The grid now picks the
   day and the hour; the popup picks the minute.

   ⚠️ Every state sets its own FOREGROUND, never inheriting body text. The
   accent and danger fills are both mid-tone, and inherited text goes muddy on
   them in one theme or the other — the cell now carries its time as text, so
   this is legibility rather than polish. Colour is never the only signal:
   each cell has a title explaining itself. */
.planner-grid {
  display: grid;
  grid-template-columns: 3.5rem repeat(7, minmax(3rem, 1fr));
  gap: 2px;
  /* Was 60vh — with the intro paragraph and legend now living above the
     grid instead of below it (2026-08-13), the grid is the last thing
     before the footer, and 60vh routinely pushed Done off-screen on
     laptop-height viewports, forcing a second, outer dialog scroll to
     reach it. The grid still scrolls internally past this height; this
     only shrinks how much of the dialog's own 86vh budget it can claim. */
  max-height: 48vh;
  overflow: auto;
  padding-right: var(--space-2);
}

.planner-corner { position: sticky; top: 0; z-index: 2; background: var(--bg); }

.planner-day {
  position: sticky; top: 0; z-index: 2;
  background: var(--bg);
  text-align: center;
  font-size: var(--text-xs);
  font-weight: 600;
  padding-bottom: 4px;
}

.planner-time {
  font-size: var(--text-xs);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
  text-align: right;
  padding-right: var(--space-2);
  line-height: 26px;
  white-space: nowrap;
  /* The dashed rule that separates one hour from the next. Light and dashed
     so it reads as a guide rather than a table border. */
  border-top: 1px dashed var(--border);
}

.planner-cell {
  /* ~30% taller than the 14px half-hour cells it replaces, and enough to
     carry "14:20" legibly. */
  min-height: 26px;
  border: 1px solid var(--border);
  border-top: 1px dashed var(--border);
  border-radius: 3px;
  padding: 0;
  cursor: pointer;
  /* Free cells carry no fill — just the border grid — rather than the
     solid --bg-inset every cell had before 2026-08-13. A block of filled
     boxes read as a spreadsheet; state (mine/blocked/partial/full) is
     still fully legible from border + the fills those states set below. */
  background: transparent;
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--text-secondary);
}
.planner-cell:hover { outline: 2px solid var(--accent); outline-offset: -1px; }
.planner-cell:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.planner-cell__t { font-weight: 600; letter-spacing: -0.01em; }

/* This campaign's slot — the only cell carrying text by default, so its
   contrast is the one that matters most. */
.planner-cell.is-mine {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast, #fff);
}
/* Free hour with a nearby slot: usable, but not from :00. Striped so it is
   distinguishable from plain free without relying on hue alone. */
.planner-cell.is-partial {
  background: repeating-linear-gradient(
    45deg, var(--bg-inset), var(--bg-inset) 4px, var(--danger-soft) 4px, var(--danger-soft) 8px
  );
  border-color: color-mix(in srgb, var(--danger) 25%, transparent);
}
.planner-cell.is-blocked {
  background: var(--danger-soft);
  border-color: color-mix(in srgb, var(--danger) 35%, transparent);
  color: var(--danger);
  cursor: not-allowed;
}
.planner-cell.is-full {
  background: var(--bg);
  border-style: dashed;
  cursor: not-allowed;
}

/* The time popup. Deliberately small — a compact Hour/Minute(/AM-PM) row
   and three buttons. */
.time-pop { max-width: 20rem; }
.time-pop__day { margin: 0 0 var(--space-3); font-size: var(--text-md); }
.time-pop__fields {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
/* select's own base rule (line ~1211) sets padding-right: 32px to clear its
   chevron — the shorthand `padding: var(--space-2)` here used to stomp that
   back down to 8px, which is what let text-lg digits run into the arrow.
   Overriding padding-right explicitly, after the shorthand, keeps the
   chevron's clearance while still tightening the other three sides. */
.time-pop__hour,
.time-pop__period {
  font-size: var(--text-base);
  font-variant-numeric: tabular-nums;
  padding: 8px var(--space-2);
  padding-right: 30px;
  width: 5rem;
  text-align: center;
  text-align-last: center;
}
.time-pop__minute {
  width: 3.4em;
  text-align: center;
  font-size: var(--text-lg);
  font-variant-numeric: tabular-nums;
  padding: 8px;
}
.time-pop__colon { font-size: var(--text-lg); color: var(--text-secondary); }
.time-pop__note { margin: var(--space-3) 0 0; min-height: 1.2em; }
.time-pop__actions {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-3);
  flex-wrap: wrap;
}

/* Sits between the intro paragraph and the grid now (moved 2026-08-13, see
   _schedule_planner.html) — margin-bottom rather than the old margin-top. */
.planner-legend {
  display: flex; flex-wrap: wrap; gap: var(--space-3);
  font-size: var(--text-xs); color: var(--text-secondary);
  margin-bottom: var(--space-3);
}
.planner-legend span { display: inline-flex; align-items: center; gap: 6px; }
.planner-legend i {
  width: 12px; height: 12px; border-radius: 3px;
  border: 1px solid var(--border); display: inline-block;
}

/* Legend swatches mirror the cell states above. Classes rather than inline
   styles so the two definitions sit next to each other and cannot drift —
   a legend that disagrees with the grid is worse than no legend. */
.planner-legend i.swatch-mine { background: var(--accent); border-color: var(--accent); }
.planner-legend i.swatch-taken { background: var(--danger-soft); border-color: color-mix(in srgb, var(--danger) 35%, transparent); }
.planner-legend i.swatch-close { background: repeating-linear-gradient(45deg, var(--bg-inset), var(--bg-inset) 3px, var(--danger-soft) 3px, var(--danger-soft) 6px); }
.planner-legend i.swatch-full { border-style: dashed; }

/* The grid needs more width than a default dialog; without this the seven
   day columns collapse to unclickable slivers. */
.dialog--wide { max-width: min(56rem, 94vw); width: 56rem; }

#global-confirm-dialog {
  width: min(420px, 92vw);
}

#global-confirm-dialog [data-confirm-message] {
  margin: 0 0 var(--space-4);
  white-space: pre-line;
}

.planner-scope {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  border: 0;
  padding: 0;
  margin: 0 0 var(--space-3);
  font-size: var(--text-sm);
}
.planner-scope legend { float: left; margin-right: var(--space-3); color: var(--text-secondary); }
.planner-scope label { display: inline-flex; align-items: center; gap: 6px; }
.planner-scope .desc { margin-left: auto; }

/* Schedule summary. With JS the text input becomes a read-only display and
   this card is the editable surface (see .schedule-card above); without JS
   the input stays a normal field and this never renders. */
.planner-summary { margin-top: var(--space-2); }
.planner-summary__empty { font-size: var(--text-sm); color: var(--text-secondary); }

/* The managed field is still submitted with its raw value — it is simply no
   longer where you type or look. Fully hidden (changed 2026-08-18, was
   dimmed-but-visible): the chip list above is a complete, correctly
   time-format-aware read of the same data, so the raw comma-joined 24h
   string added nothing once chips existed and only read as a second,
   inconsistent-looking schedule. Applied by app.js only — see
   renderSummary. Never set in the template: with JS off this field is the
   sole way to enter a schedule, and must render normally. */
input.is-planner-managed,
label.is-planner-managed { display: none; }

/* ── Combobox: a text field you can also pick from ─────────────────────────
   Settings → AI provider's Model field. Both halves are required: pick a
   known model id without typing it, and still type one that shipped after
   the last deploy.

   Not a <datalist>. That filters its options against the input's current
   value, and this field is pre-filled with a complete model id — so the list
   collapses to the single option matching it and never opens. Safari renders
   no affordance for one at all.

   The list is hidden here and revealed by app.js adding .is-open, so with JS
   off it stays shut and the input is a plain text field, which still works. */
.combobox { position: relative; display: flex; align-items: stretch; }
.combobox > input { flex: 1 1 auto; padding-right: 2.2rem; }

.combobox__toggle {
  position: absolute;
  right: 1px; top: 1px; bottom: 1px;
  width: 2rem;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: 0 var(--radius-md) var(--radius-md) 0;
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--text-sm);
  line-height: 1;
  cursor: pointer;
}
.combobox__toggle:hover { color: var(--text-primary); }

.combobox__list {
  display: none;
  position: absolute;
  top: calc(100% + 4px); left: 0; right: 0;
  z-index: 20;
  max-height: 15rem;
  overflow-y: auto;
  padding: 4px;
  background: var(--bg-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
}
.combobox.is-open .combobox__list { display: block; }

.combobox__option {
  display: block;
  width: 100%;
  text-align: left;
  padding: 6px 8px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-primary);
  font-family: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
}
.combobox__option:hover,
.combobox__option:focus {
  background: var(--accent-soft);
  color: var(--text-primary);
  outline: none;
}

/* ── Info tip: an (i) glyph that reveals a short explanation on hover, or
   on click/tap and Enter/Space (app.js's data-info-tip handler) so touch
   and keyboard users get it too. Exists so a field's rationale doesn't
   have to sit as permanent paragraph text under it. */
.info-tip {
  position: relative;
  display: inline-flex;
  vertical-align: middle;
  margin-left: 4px;
  color: var(--text-tertiary);
  cursor: help;
}
.info-tip:hover,
.info-tip:focus-visible {
  color: var(--text-secondary);
  outline: none;
}
.info-tip__popup {
  display: none;
  position: absolute;
  z-index: 20;
  /* Opens to the right rather than above the icon (changed 2026-08-18) — an
     info_tip inside a dialog title (e.g. the Weekly planner) or near the top
     of the viewport had its upward-opening popup clipped by the page header,
     since there was nothing above it to push into.

     Sideways still clips: rightward runs off a narrow viewport or the end of
     a row, and a tall popup centred on its icon clips at the top and bottom
     of the screen. app.js's positionPopup() handles both — the flip class
     below for the horizontal case, --popup-shift for the vertical one. */
  top: 50%;
  left: calc(100% + 8px);
  transform: translateY(calc(-50% + var(--popup-shift, 0px)));
  width: 260px;
  max-width: 70vw;
  padding: 10px 12px;
  background: var(--bg-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  color: var(--text-primary);
  font-size: var(--text-sm);
  font-weight: 400;
  line-height: 1.5;
  white-space: normal;
  text-align: left;
  cursor: default;
}
.info-tip:hover .info-tip__popup,
.info-tip:focus-visible .info-tip__popup,
.info-tip.is-open .info-tip__popup {
  display: block;
}
/* Flipped by app.js's positionPopup() when opening rightward would clip
   under the browser edge (an info_tip near the end of a row or the right
   edge of a narrow viewport) — opens leftward instead. */
.info-tip--flip .info-tip__popup {
  left: auto;
  right: calc(100% + 8px);
}

/* A circular on/off status indicator — an .info-tip with its icon and
   spacing overridden, not a new popup mechanism (see status_badge() in
   ui.html). Pale gray by default so it recedes next to the pills it sits
   beside; --status-badge--active is the one state worth a glance. */
.status-badge {
  width: 22px;
  height: 22px;
  margin-left: 0;
  border-radius: 50%;
  align-items: center;
  justify-content: center;
  background: var(--bg-raised);
  border: 1px solid var(--border-strong);
  color: var(--text-tertiary);
}

.status-badge:hover,
.status-badge:focus-visible {
  color: var(--text-secondary);
}

.status-badge--active {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
}

.status-badge--active:hover,
.status-badge--active:focus-visible {
  color: var(--accent);
}

/* The unassigned-@-Account prompt on /connections. One row per account:
   label, profile picker, button. Wraps rather than overflowing, because it
   sits inside an error box whose width is the page's. */
.inline-assign {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}
.inline-assign label { font-weight: 500; }
.inline-assign select { width: auto; min-width: 12rem; }

/* ── Billing: plan cards + top-up rows ──────────────────────────────────
   Both shipped as undifferentiated text inside one panel (2026-08-10): the
   three tiers ran together with nothing but a heading between them, and each
   top-up pack was a bare .btn whose entire label was the description, so a
   real purchase control read as static text. */
.plan-card {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--bg-inset);
  padding: var(--space-4) var(--space-6);
  margin-top: var(--space-4);
}

.plan-card--selected {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

.plan-card__name {
  margin: 0;
  font-size: var(--text-md);
}

.plan-card__price {
  margin: var(--space-1) 0 var(--space-2);
  font-size: var(--text-xl);
  font-weight: 600;
  color: var(--text-primary);
  line-height: 1.1;
}

.plan-card__period {
  font-size: var(--text-base);
  font-weight: 400;
  color: var(--text-secondary);
}

/* The one X-credit caveat, stated once above the tiers. Accent text on its
   own line rather than a pill per card — a pill in a feature list reads as a
   badge the tier earned rather than a condition that applies to all of them. */
.billing-note {
  margin: 0;
  font-size: 13px;
  font-weight: 500;
  color: var(--accent);
}

/* ── plan-bento ───────────────────────────────────────────────────────
   Settings > Plans' offer grid: top-up packs and plan tiers as individual
   cards in a fixed two-column layout, not two panels stacked full-width. A
   fixed 1fr/1fr rather than settings-grid's auto-fit + minmax is
   deliberate — this needs exactly two columns so a pair of top-up packs
   stays genuinely side by side instead of a wide viewport fitting three or
   four across. A section's intro (.panel) and each plan tier (.plan-card)
   both carry .span-2 to claim a full row; a top-up pack carries no span,
   so pairs of packs fall into the same row automatically via grid
   auto-placement. */
.plan-bento {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

.plan-bento > .panel,
.plan-bento > .plan-card {
  margin-bottom: 0;
  margin-top: 0;
}

.plan-bento > .span-2 {
  grid-column: 1 / -1;
}

/* --- guided brief word meter ---------------------------------------------
   Three bands: below the minimum the form will be refused, above it the
   campaign can be created, and past the "good" mark there is enough material
   for the drip to rotate through without looping quickly.

   The thresholds themselves are rendered server-side from
   documents.MIN_DOCUMENT_WORDS and brief_composer.GOOD_DOCUMENT_WORDS — none
   of the numbers live here, so a band can never promise a limit the server
   does not enforce.

   Colour carries no information on its own: the count and the state word next
   to it say the same thing in text. Uses the same primitives as the pills so
   it themes with everything else. */
.word-meter__track {
  position: relative;
  height: 6px;
  border-radius: 3px;
  background: var(--colorNeutralBackground3, rgba(127, 127, 127, 0.22));
  overflow: hidden;
}
.word-meter__fill {
  height: 100%;
  width: 0;
  border-radius: 3px;
  background: var(--p-danger);
  transition: width 120ms linear, background-color 120ms linear;
}
.word-meter[data-state="ok"] .word-meter__fill { background: var(--p-warning); }
.word-meter[data-state="good"] .word-meter__fill { background: var(--p-success); }
/* The minimum, marked on the track so the jump from red to orange has a
   visible reason rather than appearing to happen at an arbitrary point. */
.word-meter__mark {
  position: absolute;
  top: -2px;
  bottom: -2px;
  width: 2px;
  background: var(--colorNeutralForeground3, currentColor);
  opacity: 0.55;
}
.word-meter__label {
  margin: 6px 0 0;
  font-size: 12px;
  color: var(--colorNeutralForeground3, inherit);
}
.word-meter[data-state="short"] .word-meter__label [data-word-state] { color: var(--p-danger); }
.word-meter[data-state="ok"] .word-meter__label [data-word-state] { color: var(--p-warning); }
.word-meter[data-state="good"] .word-meter__label [data-word-state] { color: var(--p-success); }
@media (prefers-reduced-motion: reduce) {
  .word-meter__fill { transition: none; }
}
