/* ===========================================================================
   SESKit design system
   ---------------------------------------------------------------------------
   Hand-authored: no build step, no Node toolchain, no 112MB binary. That is a
   deliberate choice - the product's whole pitch is one lightweight Python
   service, and the build pipeline should not contradict it.

   Direction and rationale live in docs/design/system.md. Read that before
   adding to this file.

   Structure:
     1. Tokens (light / dark / explicit override)
     2. Reset and base
     3. Layout primitives
     4. Components
     5. Utilities
   =========================================================================== */

/* --------------------------------------------------------------- 1. Tokens */

:root {
  /* Neutrals carry a slight blue bias - a pure grey reads as unconsidered.  */
  --bg: #f7f8fa;
  --surface: #ffffff;
  --surface-raised: #ffffff;
  --surface-hover: #f4f6f9;
  /* A well inside a card, for a code block - the one surface that has to sit
     *under* the card rather than on it. Raised is the wrong direction for it
     and in this theme also the wrong colour: --surface-raised is white, the
     same white as the card, so a code block had nothing but its border. */
  --surface-sunken: #f2f4f7;

  --border: #e4e7ec;
  --border-strong: #d0d5dd;

  --text: #14181f;
  --text-muted: #5a6472;
  --text-subtle: #8a94a3;

  /* The brand blue, sampled from the logo mark. Restrained: primary actions
     and active nav state only. White on it is 4.93:1 and it is 4.64:1 against
     the page background - both AA, and both tighter than the indigo this
     replaced, so darkening it further is the safe direction if it ever moves. */
  --accent: #0166f8;
  --accent-hover: #0154cf;
  --accent-subtle: #e8f1ff;
  --accent-border: #b9d4ff;
  --accent-contrast: #ffffff;

  /* Semantic colours are for delivery state, never for decoration.          */
  --success: #067647;
  --success-bg: #ecfdf3;
  --success-border: #abefc6;

  --warning: #b54708;
  --warning-bg: #fffaeb;
  --warning-border: #fedf89;

  --danger: #b42318;
  --danger-bg: #fef3f2;
  --danger-border: #fecdca;

  --neutral: #475467;
  --neutral-bg: #f2f4f7;
  --neutral-border: #e4e7ec;

  --focus-ring: #0166f8;

  /* The chevron a <select> draws for itself, drawn by us instead.

     A data URI cannot read a custom property, so the stroke colour is
     baked in and each theme carries its own - which is why this is a token
     rather than a value sitting in the component. Matches the icon set:
     stroke-based, 1.5px, round caps, on a 24px grid. */
  --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235a6472' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9.5 6 6 6-6'/%3E%3C/svg%3E");

  /* Type. IBM Plex where present - designed for technical products - with a
     system stack behind it. No webfont request: a self-hosted dashboard may
     run with no internet access at all.                                     */
  --font-sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text",
    "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: "IBM Plex Mono", "JetBrains Mono", ui-monospace, "SF Mono", "Cascadia Mono",
    Menlo, Consolas, monospace;

  --text-xs: 0.75rem;
  --text-sm: 0.8125rem;
  --text-base: 0.875rem;
  --text-md: 1rem;
  --text-lg: 1.125rem;
  --text-xl: 1.375rem;
  --text-2xl: 1.75rem;
  --text-3xl: 2.25rem;

  /* Space scale, 4px base. */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.25rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-10: 2.5rem;
  --space-12: 3rem;
  --space-16: 4rem;

  /* One height for anything a pointer lands on.

     There were five: a 32px button, a 34px input, a 30px select, a 36px auth
     submit and a 26px small button. Every `row row--end` form on the dashboard
     bottom-aligns a button against an input, so two of those sat side by side
     with the button 2px proud of the field - which is most of what "the
     spacing looks off" turns out to mean.

     A scale of three rather than one value: a table's row action and a
     full-width sign-in button genuinely are not the same control. Three is a
     decision; five was an accident. */
  --control-h-sm: 28px;
  --control-h: 34px;
  --control-h-lg: 40px;

  /* Restrained radii - not everything is a pill. */
  --radius-sm: 4px;
  --radius: 6px;
  --radius-lg: 8px;
  --radius-full: 999px;

  /* Borders do most of the separation work; shadows stay near-invisible. */
  --shadow-sm: 0 1px 2px rgb(16 24 40 / 0.04);
  --shadow: 0 1px 3px rgb(16 24 40 / 0.06), 0 1px 2px rgb(16 24 40 / 0.04);
  --shadow-lg: 0 4px 12px rgb(16 24 40 / 0.08), 0 2px 4px rgb(16 24 40 / 0.04);

  --sidebar-width: 248px;
  --content-max: 1200px;
  --topbar-height: 56px;
}

/* System dark. Guarded so an explicit light choice still wins on a dark OS. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #0b0d11;
    --surface: #14171d;
    --surface-raised: #191d24;
    --surface-hover: #1c2129;
    --surface-sunken: #0d1015;

    --border: #232830;
    --border-strong: #2f3641;

    --text: #e8ecf2;
    --text-muted: #98a2b3;
    --text-subtle: #6b7686;

    /* The same hue lifted for a dark surface - 5.98:1, matching the tone the
       indigo held here rather than simply reusing the light-theme value, which
       would fail against #14171d. */
    --accent: #4d94ff;
    --accent-hover: #6aa5ff;
    --accent-subtle: #0a1a33;
    --accent-border: #17356b;
    --accent-contrast: #0b0d11;

    --success: #47cd89;
    --success-bg: #0a2a1c;
    --success-border: #164c33;

    --warning: #fdb022;
    --warning-bg: #2e1e05;
    --warning-border: #573a08;

    --danger: #f97066;
    --danger-bg: #2e1110;
    --danger-border: #5b1d18;

    --neutral: #98a2b3;
    --neutral-bg: #1a1f26;
    --neutral-border: #2a313b;

    --focus-ring: #4d94ff;

    --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2398a2b3' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9.5 6 6 6-6'/%3E%3C/svg%3E");

    --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.3);
    --shadow: 0 1px 3px rgb(0 0 0 / 0.4), 0 1px 2px rgb(0 0 0 / 0.3);
    --shadow-lg: 0 4px 12px rgb(0 0 0 / 0.5), 0 2px 4px rgb(0 0 0 / 0.3);
  }
}

/* Explicit dark choice - wins over a light OS preference. */
:root[data-theme="dark"] {
  --bg: #0b0d11;
  --surface: #14171d;
  --surface-raised: #191d24;
  --surface-hover: #1c2129;
  --surface-sunken: #0d1015;

  --border: #232830;
  --border-strong: #2f3641;

  --text: #e8ecf2;
  --text-muted: #98a2b3;
  --text-subtle: #6b7686;

  --accent: #4d94ff;
  --accent-hover: #6aa5ff;
  --accent-subtle: #0a1a33;
  --accent-border: #17356b;
  --accent-contrast: #0b0d11;

  --success: #47cd89;
  --success-bg: #0a2a1c;
  --success-border: #164c33;

  --warning: #fdb022;
  --warning-bg: #2e1e05;
  --warning-border: #573a08;

  --danger: #f97066;
  --danger-bg: #2e1110;
  --danger-border: #5b1d18;

  --neutral: #98a2b3;
  --neutral-bg: #1a1f26;
  --neutral-border: #2a313b;

  --focus-ring: #4d94ff;

  --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2398a2b3' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9.5 6 6 6-6'/%3E%3C/svg%3E");

  --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.3);
  --shadow: 0 1px 3px rgb(0 0 0 / 0.4), 0 1px 2px rgb(0 0 0 / 0.3);
  --shadow-lg: 0 4px 12px rgb(0 0 0 / 0.5), 0 2px 4px rgb(0 0 0 / 0.3);
}

/* ------------------------------------------------------- 2. Reset and base */

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

* {
  margin: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  color-scheme: light dark;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-feature-settings: "cv05" 1;
}

h1,
h2,
h3,
h4 {
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.011em;
  text-wrap: balance;
}

h1 { font-size: var(--text-2xl); letter-spacing: -0.02em; }
h2 { font-size: var(--text-xl); letter-spacing: -0.015em; }
h3 { font-size: var(--text-md); }
h4 { font-size: var(--text-base); }

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

a:hover { text-decoration: underline; }

code,
kbd,
pre,
samp {
  font-family: var(--font-mono);
  font-size: 0.9em;
}

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

img,
svg {
  display: block;
  max-width: 100%;
}

/* `hidden` has to beat a component class.

   The UA sheet's `[hidden] { display: none }` is a single attribute selector,
   so any class that sets `display` outruns it - `.btn` is `inline-flex`, and a
   hidden button carrying it stays on screen. Restated once, with weight, so
   `el.hidden = true` means the same thing everywhere. */
[hidden] {
  display: none !important;
}

/* One visible focus treatment everywhere - keyboard access is a requirement,
   not a nice-to-have. */
:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

:focus:not(:focus-visible) { outline: none; }

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

/* --------------------------------------------------- 3. Layout primitives */

.app {
  display: grid;
  grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
  min-height: 100vh;
}

/* Sidebar ----------------------------------------------------------------- */

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

.sidebar__brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0 var(--space-2);
  font-size: var(--text-md);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--text);
}

.sidebar__brand:hover { text-decoration: none; }

/* The logo mark, which replaced a lettered square standing in for it.
   The icon is the only supplied asset with an alpha channel, so it is the only
   one that can sit on the sidebar in both themes - the horizontal and stacked
   wordmarks carry a baked white background and would show as a white block in
   dark mode. The word "SESKit" beside it is therefore set in the interface
   type rather than being part of an image, which also keeps it selectable,
   sharp at any zoom, and recoloured by the theme for free. */
.sidebar__mark {
  width: 24px;
  height: 24px;
  flex-shrink: 0;
  /* Explicit dimensions so the row does not reflow when the image loads. */
}

/* Two marks, one shown.

   The sidebar follows the surface rather than the accent, so the mark cannot
   be one file: the blue one measures 3.64:1 on the dark surface where the pale
   one measures 15.77:1, and the pale one is unreadable on white.

   Both are in the markup and CSS picks. That means the wrong one is fetched
   too - 8KB, once, cached - which buys a swap that happens before first paint
   with no flash and no JavaScript. The ordering below matters: the explicit
   [data-theme] rules come last so a user who chose light inside a dark OS gets
   the light mark, which is the case the media query alone gets wrong. */
.sidebar__mark--dark,
.auth__mark--dark {
  display: none;
}

@media (prefers-color-scheme: dark) {
  .sidebar__mark--light,
  .auth__mark--light {
    display: none;
  }

  .sidebar__mark--dark,
  .auth__mark--dark {
    display: block;
  }
}

[data-theme="dark"] .sidebar__mark--light,
[data-theme="dark"] .auth__mark--light {
  display: none;
}

[data-theme="dark"] .sidebar__mark--dark,
[data-theme="dark"] .auth__mark--dark {
  display: block;
}

[data-theme="light"] .sidebar__mark--light,
[data-theme="light"] .auth__mark--light {
  display: block;
}

[data-theme="light"] .sidebar__mark--dark,
[data-theme="light"] .auth__mark--dark {
  display: none;
}

.sidebar__nav {
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.sidebar__section {
  padding: var(--space-4) var(--space-2) var(--space-2);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-subtle);
}

.nav-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-2);
  border-radius: var(--radius);
  color: var(--text-muted);
  font-size: var(--text-base);
  font-weight: 450;
  transition: background-color 0.12s ease, color 0.12s ease;
}

.nav-item:hover {
  background: var(--surface-hover);
  color: var(--text);
  text-decoration: none;
}

.nav-item[aria-current="page"] {
  background: var(--accent-subtle);
  color: var(--accent);
  font-weight: 500;
}

.nav-item__icon {
  flex: none;
  width: 16px;
  height: 16px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.5;
}

.sidebar__footer {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}

/* Main column ------------------------------------------------------------- */

.main {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  height: var(--topbar-height);
  padding: 0 var(--space-6);
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  position: sticky;
  top: 0;
  z-index: 10;
}

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

.content {
  flex: 1;
  width: 100%;
  max-width: var(--content-max);
  padding: var(--space-8) var(--space-6) var(--space-16);
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

.page-header {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.page-header__subtitle {
  color: var(--text-muted);
  font-size: var(--text-base);
}

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* A message's delivery timeline (§17). A rail rather than a plain list, so the
   entries read as one sequence; it is decoration over `.stack`, which still
   lays the items out on its own if this rule is ever dropped. */
.timeline {
  border-left: 1px solid var(--border);
  padding-left: var(--space-4);
}

.timeline__item {
  position: relative;
}

.timeline__item::before {
  content: "";
  position: absolute;
  left: calc(-1 * var(--space-4) - 4px);
  top: 0.5rem;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--border-strong);
}

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

/* auto-fill, not auto-fit.

   auto-fit collapses the empty tracks on the last row and hands their width to
   whatever is on it, so six tiles in four columns end as four of one size and
   two of another. auto-fill keeps the tracks, and every tile is the same width
   at every window size - which is the whole job of a grid of counts.

   The tradeoff auto-fill makes is a ragged right edge on a part-full row, and
   220px is chosen so the two grids that exist land well: the Emails page's
   four counts fill a row exactly at the content width, and the Overview's six
   fall as four and two. Equal tiles with a gap beside them, rather than a full
   row of two different sizes. */
.grid-metrics {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--space-4);
}

/* ----------------------------------------------------------- 4. Components */

/* Card -------------------------------------------------------------------- */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}

.card__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--border);
}

.card__title {
  font-size: var(--text-base);
  font-weight: 600;
}

/* A card body lays its children out; it does not just pad them.

   The reset sets `* { margin: 0 }`, and this was a plain padded block, so
   anything a page put inside it touched whatever came next. Every card with
   more than one child was affected and the Webhooks page was the worst of
   them: a description list, a row of three buttons, a paragraph, a heading and
   a table, all flush against each other.

   Fixed here rather than by wrapping each page's contents in `.stack`, because
   the pages were not doing anything wrong - a card that only pads is. */
.card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-5);
}

.card--flush .card__body { padding: 0; }

/* A card that folds ------------------------------------------------------- */

/* <details> and <summary> rather than a button and a script: it folds with no
   JavaScript at all, it is keyboard-operable and announced as expanded or
   collapsed without any ARIA of ours, and it survives a blocked script - which
   for this dashboard is not hypothetical, since the CSP refuses inline ones. */

.card--collapsible > .card__header {
  cursor: pointer;
  /* The UA marker is replaced by our own chevron, which can be themed and
     rotated. Two rules, because Safari uses the pseudo-element and the rest
     use `list-style`. */
  list-style: none;
  user-select: none;
}

.card--collapsible > .card__header::-webkit-details-marker {
  display: none;
}

.card--collapsible > .card__header:hover {
  background: var(--surface-hover);
}

/* The border under the header belongs to the open state. Closed, it is a line
   under nothing. */
.card--collapsible:not([open]) > .card__header {
  border-bottom: none;
}

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

.card__meta {
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.card__chevron {
  flex: none;
  width: 16px;
  height: 16px;
  stroke: var(--text-subtle);
  fill: none;
  stroke-width: 1.5;
  transition: transform 0.15s ease;
}

.card--collapsible[open] > .card__header .card__chevron {
  transform: rotate(180deg);
}

/* A heading inside a body starts a new section, so it wants more room above it
   than the even rhythm gives. Only when something precedes it - a first child
   is not a break from anything. */
.card__body > h3:not(:first-child) {
  margin-top: var(--space-2);
}

/* Metric tile ------------------------------------------------------------- */

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

.metric__label {
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-subtle);
}

.metric__value {
  font-size: var(--text-2xl);
  font-weight: 600;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
}

.metric__meta {
  font-size: var(--text-sm);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* Button ------------------------------------------------------------------ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: 0 var(--space-3);
  height: var(--control-h);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font-size: var(--text-base);
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}

.btn:hover {
  background: var(--surface-hover);
  text-decoration: none;
}

.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
}

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

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

.btn--ghost {
  background: transparent;
  border-color: transparent;
  color: var(--text-muted);
}

.btn--ghost:hover {
  background: var(--surface-hover);
  color: var(--text);
}

.btn--danger {
  background: var(--surface);
  border-color: var(--danger-border);
  color: var(--danger);
}

.btn--danger:hover { background: var(--danger-bg); }

.btn--sm { height: var(--control-h-sm); font-size: var(--text-sm); padding: 0 var(--space-2); }
.btn--icon { width: var(--control-h); padding: 0; }
.btn--icon.btn--sm { width: var(--control-h-sm); }

.btn__icon {
  width: 15px;
  height: 15px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.5;
  flex: none;
}

/* Badge ------------------------------------------------------------------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 1px var(--space-2);
  border: 1px solid var(--neutral-border);
  border-radius: var(--radius-full);
  background: var(--neutral-bg);
  color: var(--neutral);
  font-size: var(--text-xs);
  font-weight: 500;
  white-space: nowrap;
}

/* The dot encodes state in form as well as colour, so status still reads
   without relying on colour perception. */
.badge__dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

.badge--success {
  background: var(--success-bg);
  border-color: var(--success-border);
  color: var(--success);
}

.badge--warning {
  background: var(--warning-bg);
  border-color: var(--warning-border);
  color: var(--warning);
}

.badge--danger {
  background: var(--danger-bg);
  border-color: var(--danger-border);
  color: var(--danger);
}

.badge--accent {
  background: var(--accent-subtle);
  border-color: var(--accent-border);
  color: var(--accent);
}

/* Input ------------------------------------------------------------------- */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.field__label {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text);
}

.field__hint {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* A field whose control has an action on its line - "Name [ ... ] [Create]".

   The button used to sit outside the field, in a `row--end`, which aligns on
   the bottom. A field is label, control *and* hint, so the bottom it aligned
   to was the hint's: the button came out 28px below the input it submits, on
   four pages. Inside the field it is beside the control, which is the thing it
   acts on, and the hint explains both from underneath. */
.field__control {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.field__control .input {
  flex: 1 1 auto;
  min-width: 0;
}

.field__error {
  font-size: var(--text-sm);
  color: var(--danger);
}

.input {
  width: 100%;
  height: var(--control-h);
  padding: 0 var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font-size: var(--text-base);
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}

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

.input:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-subtle);
}

.input[aria-invalid="true"] { border-color: var(--danger); }

.input--mono { font-family: var(--font-mono); font-size: var(--text-sm); }

/* Textarea ---------------------------------------------------------------- */

/* Shares the input's box and focus ring, and differs in the two ways it has
   to: it has a height rather than a line, and it may be dragged taller but not
   wider - a horizontally resizable field escapes its column and takes the
   layout with it. */
.textarea {
  width: 100%;
  min-height: 7rem;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: var(--text-base);
  line-height: 1.5;
  resize: vertical;
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}

.textarea::placeholder { color: var(--text-subtle); }

.textarea:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-subtle);
}

/* Select ------------------------------------------------------------------ */

/* Both dropdowns wore `.input`, which is a rule written for a text field:
   symmetric padding and no `appearance`. The browser then paints its own
   chevron over that right-hand padding and sizes the control to its own taste,
   so the two selects in the dashboard looked like neither each other nor
   anything else on the page.

   `appearance: none` takes the native control away and the background paints
   the chevron back. A pseudo-element could not: a <select> renders only its
   options, and ::after on one is never drawn. */

.select {
  width: 100%;
  height: var(--control-h);
  /* Asymmetric on purpose - the right side is holding the chevron. */
  padding: 0 var(--space-8) 0 var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background-color: var(--surface);
  background-image: var(--select-chevron);
  background-repeat: no-repeat;
  background-position: right var(--space-3) center;
  background-size: 16px 16px;
  color: var(--text);
  font-size: var(--text-base);
  appearance: none;
  cursor: pointer;
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}

/* The same focus treatment as .input, written out rather than shared through a
   grouped selector: these are two components, and one of them growing a
   different focus ring later should not be a rewrite of both. */
.select:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-subtle);
}

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

/* An <option> is drawn by the platform, not by this stylesheet. Naming the
   colours is the one thing that can be done about a dark theme's open dropdown
   rendering as black on white. */
.select option {
  background: var(--surface);
  color: var(--text);
}

/* Table ------------------------------------------------------------------- */

/* Wide content scrolls inside its own container - the page body never scrolls
   sideways. */
.table-wrap {
  width: 100%;
  overflow-x: auto;
}

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-base);
}

.table th {
  padding: var(--space-2) var(--space-5);
  text-align: left;
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-subtle);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

.table td {
  padding: var(--space-3) var(--space-5);
  border-bottom: 1px solid var(--border);
  color: var(--text);
  vertical-align: middle;
}

.table tbody tr:last-child td { border-bottom: none; }

.table tbody tr:hover { background: var(--surface-hover); }

.table__num { font-variant-numeric: tabular-nums; text-align: right; }
.table__mono { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--text-muted); }

/* Empty state ------------------------------------------------------------- */

/* Given real design attention: for a new install this is the first thing a
   user sees on every page, so it has to teach rather than apologise. */
.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-12) var(--space-6);
  text-align: center;
}

.empty__icon {
  width: 32px;
  height: 32px;
  stroke: var(--text-subtle);
  fill: none;
  stroke-width: 1.25;
}

.empty__title {
  font-size: var(--text-md);
  font-weight: 600;
  color: var(--text);
}

.empty__body {
  max-width: 46ch;
  color: var(--text-muted);
  text-wrap: pretty;
}

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

/* Code block -------------------------------------------------------------- */

.code {
  position: relative;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface-sunken);
  overflow: hidden;
}

.code__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--border);
  font-size: var(--text-xs);
  color: var(--text-subtle);
}

.code pre {
  margin: 0;
  padding: var(--space-4);
  overflow-x: auto;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--text);
}

/* Loading ----------------------------------------------------------------- */

/* HTMX toggles this automatically on any element with hx-indicator. */
.htmx-indicator {
  opacity: 0;
  transition: opacity 0.15s ease;
}

.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
  opacity: 1;
}

.spinner {
  width: 14px;
  height: 14px;
  border: 2px solid var(--border-strong);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
}

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

.skeleton {
  background: linear-gradient(
    90deg,
    var(--surface-hover) 25%,
    var(--border) 37%,
    var(--surface-hover) 63%
  );
  background-size: 400% 100%;
  border-radius: var(--radius-sm);
  animation: shimmer 1.4s ease infinite;
}

@keyframes shimmer {
  0% { background-position: 100% 50%; }
  100% { background-position: 0 50%; }
}

/* Auth pages -------------------------------------------------------------- */

.auth {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  min-height: 100vh;
  padding: var(--space-6);
  background: var(--bg);
}

.auth__card {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  width: 100%;
  max-width: 380px;
  padding: var(--space-8) var(--space-6);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
}

.auth__brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-md);
  font-weight: 600;
  letter-spacing: -0.02em;
}

.auth__mark {
  width: 32px;
  height: 32px;
  flex-shrink: 0;
}

.auth__header {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.auth__title {
  font-size: var(--text-xl);
  font-weight: 600;
  letter-spacing: -0.015em;
}

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

/* The one control on the page, and the reason for the large step: a
   full-width primary action at the same height as a table's row button reads
   as a form that has not finished loading. */
.auth__submit {
  width: 100%;
  height: var(--control-h-lg);
  margin-top: var(--space-1);
}

.auth__footnote {
  max-width: 380px;
  color: var(--text-subtle);
  font-size: var(--text-sm);
  text-align: center;
  text-wrap: pretty;
}

/* Alert -------------------------------------------------------------------- */

.alert {
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--neutral-border);
  border-radius: var(--radius);
  background: var(--neutral-bg);
  color: var(--neutral);
  font-size: var(--text-base);
  text-wrap: pretty;
}

.alert--danger {
  background: var(--danger-bg);
  border-color: var(--danger-border);
  color: var(--danger);
}

.alert--success {
  background: var(--success-bg);
  border-color: var(--success-border);
  color: var(--success);
}

/* User menu ---------------------------------------------------------------- */

.usermenu {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding-left: var(--space-2);
  border-left: 1px solid var(--border);
}

.usermenu__email {
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

/* Project switcher --------------------------------------------------------- */

.switcher {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: 0 var(--space-2);
}

.switcher__label {
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-subtle);
}

.switcher__current {
  font-size: var(--text-base);
  font-weight: 500;
  color: var(--text);
}

/* Only on screen when the script that submits on change did not arrive, so it
   is sized like a control rather than stretched across the sidebar. */
.switcher__submit {
  align-self: flex-start;
  margin-top: var(--space-1);
}

/* Skip link.
   Hidden the same way .visually-hidden hides things, but with a way back:
   .visually-hidden has no :focus escape, and it must not grow one, because it
   is also what hides the "View " prefix inside table links. A skip link nobody
   can see when they tab to it helps a screen reader and abandons every other
   keyboard user, which is the half more likely to be watching the screen. */
.skip-link {
  position: absolute;
  left: var(--space-4);
  top: var(--space-4);
  z-index: 100;
  padding: var(--space-2) var(--space-4);
  background: var(--surface-raised);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  font-size: var(--text-sm);
  text-decoration: none;
  transform: translateY(calc(-100% - var(--space-4) - 4px));
}

.skip-link:focus-visible {
  transform: translateY(0);
}

/* Toast.
   Server-rendered in the document, then floated by app.js. Without scripts it
   stays in flow at the top of the page, which is why the positioning lives on
   the [data-toast-floating] state rather than on .toast itself - a message
   that scripts never reached must not end up fixed over the content with no
   way to dismiss it. */
.toast {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-4);
  padding: var(--space-3) var(--space-4);
  background: var(--success-bg);
  color: var(--text);
  border: 1px solid var(--success-border);
  border-radius: var(--radius);
  font-size: var(--text-sm);
}

.toast[data-toast-floating] {
  position: fixed;
  right: var(--space-6);
  bottom: var(--space-6);
  left: auto;
  z-index: 200;
  margin: 0;
  max-width: min(380px, calc(100vw - var(--space-12)));
  background: var(--surface-raised);
  border-color: var(--border-strong);
  box-shadow: var(--shadow-lg);
  animation: toast-in 160ms ease-out;
}

.toast__text {
  flex: 1;
}

.toast__close {
  flex-shrink: 0;
  padding: 0 var(--space-1);
  background: none;
  border: 0;
  color: var(--text-muted);
  font-size: var(--text-lg);
  line-height: 1;
  cursor: pointer;
}

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

@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(var(--space-3));
  }
}

/* No reduced-motion block for the toast: the global reset near the top of the
   file already flattens every animation, and a second rule saying so invites
   the belief that animations elsewhere are unguarded. */

/* Sits beside a segmented control, so it needs to not shift the row when it
   appears - .htmx-indicator fades opacity rather than toggling display for
   exactly that reason. */
.segmented-busy {
  display: inline-flex;
  align-items: center;
  margin-left: var(--space-3);
}

/* ------------------------------------------------------------ 5. Utilities */

.muted { color: var(--text-muted); }
.subtle { color: var(--text-subtle); }
.mono { font-family: var(--font-mono); }
.tabular { font-variant-numeric: tabular-nums; }
.text-sm { font-size: var(--text-sm); }
.text-xs { font-size: var(--text-xs); }

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

/* ---------------------------------------------------------- 6. Responsive */

@media (max-width: 900px) {
  .app { grid-template-columns: minmax(0, 1fr); }

  .sidebar {
    position: static;
    height: auto;
    flex-direction: row;
    align-items: center;
    gap: var(--space-4);
    overflow-x: auto;
    border-right: none;
    border-bottom: 1px solid var(--border);
  }

  .sidebar__nav { flex-direction: row; }

  /* The section label goes, because a heading over a horizontal bar labels
     nothing. The footer stays: it is Settings and Docs, and hiding it took two
     destinations out of reach on a phone with nothing else linking to them. */
  .sidebar__section { display: none; }

  .sidebar__footer {
    flex-direction: row;
    margin-top: 0;
    padding-top: var(--space-1);
    padding-left: var(--space-3);
    /* A divider on the side it is now on. */
    border-top: none;
    border-left: 1px solid var(--border);
  }

  /* The bar scrolls, so nothing in it should be compressed to fit - that is
     what the scrolling is for. */
  .sidebar__nav,
  .sidebar__footer,
  .nav-item,
  .switcher {
    flex: none;
  }

  .nav-item { white-space: nowrap; }

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

/* --------------------------------------------------------------- api keys */

/* Aligns a submit button with the bottom of the field beside it, rather than
   with the middle of the label-plus-input stack. */
.row--end { align-items: flex-end; }
.row--end .field { margin: 0; }

/* Fields side by side line up on their labels, not their hints. Bottom-aligned,
   two fields whose hints wrap to different numbers of lines put their inputs at
   different heights - which is the same bug as the button, one row over. */
.row--start { align-items: flex-start; }

.grow { flex: 1 1 auto; min-width: 0; }

.table__actions { text-align: right; white-space: nowrap; }
.table__actions form { display: inline; }

/* A revoked key stays in the table as a record, but must not read as usable. */
.is-revoked td { color: var(--text-subtle); }
.is-revoked code { opacity: 0.6; }

/* The show-once panel. Warning-toned on purpose: this is the only moment the
   raw key exists, and a user who scrolls past it has lost it. */
.keyreveal {
  margin-bottom: var(--space-5);
  padding: var(--space-4) var(--space-5);
  background: var(--warning-bg);
  border: 1px solid var(--warning-border);
  border-radius: var(--radius);
}

.keyreveal__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}

.keyreveal__title { font-weight: 600; color: var(--text); }

.keyreveal__note {
  margin: 0 0 var(--space-3);
  font-size: var(--text-sm);
  color: var(--warning);
}

.keyreveal__value {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  background: var(--surface);
  border: 1px solid var(--warning-border);
  border-radius: var(--radius-sm);
}

/* The key is long and must wrap rather than widen the page on a narrow
   window. break-all because there is no word boundary to break on. */
.keyreveal__key {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--text);
  word-break: break-all;
}

/* Alert: warning tone and an optional heading ------------------------------ */
/* The sandbox notice (§8) has to stay legible while it sits on the page
   indefinitely, so it uses the same warning tokens as the badge rather than a
   colour invented for it. */

.alert--warning {
  background: var(--warning-bg);
  border-color: var(--warning-border);
  color: var(--warning);
}

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

.alert p + p {
  margin-top: var(--space-2);
}

/* Description list --------------------------------------------------------- */
/* Label/value pairs that are read, not scanned - an AWS account id, a region.
   A table would imply rows worth comparing across; these are attributes of one
   thing. */

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

.datalist__row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  align-items: baseline;
}

.datalist__row dt {
  flex: 0 0 10rem;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.datalist__row dd {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  min-width: 0;
}

@media (max-width: 40rem) {
  .datalist__row dt {
    flex-basis: 100%;
  }
}

/* Inline link -------------------------------------------------------------- */

.link {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.link:hover {
  text-decoration-thickness: 2px;
}

/* Segmented control -------------------------------------------------------- */
/* The Overview's time range. Links rather than buttons, so a range is a view
   that can be linked to and survives a refresh - HTMX upgrades them to a
   fragment swap, and without it they are ordinary navigation that still works.
   Sized and coloured entirely from tokens, like every other component. */

.segmented {
  display: inline-flex;
  align-items: center;
  gap: 1px;
  padding: 2px;
  background: var(--neutral-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.segmented__item {
  display: inline-flex;
  align-items: center;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  text-decoration: none;
  white-space: nowrap;
}

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

.segmented__item.is-active {
  color: var(--text);
  background: var(--surface);
  border-color: var(--border-strong);
  box-shadow: var(--shadow-sm);
}

.segmented__item:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 1px;
}

/* Chart -------------------------------------------------------------------- */
/* A fixed height so the canvas has something to be responsive *within* - a
   Chart.js canvas in an auto-height box grows on every resize event. */

.chart {
  position: relative;
  height: 16rem;
  margin-top: var(--space-2);
}

@media (max-width: 40rem) {
  .chart {
    height: 12rem;
  }
}

/* Setup checklist ---------------------------------------------------------
   Shown on the Overview until every step is done. Deliberately quiet: it is
   the first thing on a new install and the last thing anybody wants to see
   once they are working, so it looks like a list rather than a banner. */

.setup {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.setup__step {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
}

/* Fixed width so the titles line up whether or not a step is done - a list
   whose left edge moves as you complete it reads as two different lists. */
/* Fixed width so the titles line up whether a step shows a number or a tick -
   a list whose left edge moves as you complete it reads as two different
   lists. Tabular figures for the same reason, at two digits. */
.setup__mark {
  flex: 0 0 1.5rem;
  text-align: center;
  color: var(--text-subtle);
  font-size: var(--text-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1.5;
}

/* The tick earns the colour; a step number is not a success. */
.setup__step--done .setup__mark {
  color: var(--success);
  font-size: var(--text-base);
}

.setup__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.setup__title {
  font-weight: 500;
}

/* Struck through rather than removed. Seeing what you already did is most of
   what makes a checklist feel like progress instead of a list of demands. */
.setup__step--done .setup__title {
  color: var(--text-muted);
  text-decoration: line-through;
}

/* The same size as `.field__hint`, which is the same thing: the line under a
   label saying why it is worth doing. */
.setup__why {
  font-size: var(--text-sm);
}
