/* ─── Vendored webfonts ─────────────────────────────────────────────
   Self-hosted so the UI has no CDN dependency (see CLAUDE.md). These are
   Google Fonts' own pre-subset *variable* woff2 files, one per unicode-range,
   so a single file covers the whole weight range:

     Inter  v20  weights 400-700  — SIL Open Font License 1.1
     Outfit v15  weights 600-700  — SIL Open Font License 1.1

   Fetched from the URLs in https://fonts.googleapis.com/css2?family=Inter:wght@400..700
   and ...family=Outfit:wght@600..700 (request with a modern browser User-Agent to
   get woff2). To refresh: re-fetch that CSS and download the `latin` and
   `latin-ext` src URLs, keeping the filenames below.

   Filenames are all-lowercase on purpose: macOS is case-insensitive but the
   deploy container is not, so a mismatch would pass locally and 404 in prod.
   `src` is relative because plain CSS has no {% static %} — whitenoise's
   manifest storage rewrites it to the hashed name at collectstatic time, which
   also means each file must exist at exactly this path or the collect aborts. */

/* Inter — latin */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("../fonts/inter-latin.65850a373e25.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Inter — latin-ext */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("../fonts/inter-latin-ext.01ba6c2a184b.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Outfit — latin */
@font-face {
  font-family: 'Outfit';
  font-style: normal;
  font-weight: 600 700;
  font-display: swap;
  src: url("../fonts/outfit-latin.98ccc2b7f189.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Outfit — latin-ext */
@font-face {
  font-family: 'Outfit';
  font-style: normal;
  font-weight: 600 700;
  font-display: swap;
  src: url("../fonts/outfit-latin-ext.e2b64ddcb351.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ─── Design tokens ─────────────────────────────────────────────────
   Palette shared with the Phoenix mobile app: a deep navy frame around a
   light workspace, with orange as the single "you can act here" signal.

   Every contrast ratio noted below was measured, not estimated. Two rules
   are load-bearing and easy to break by accident:

     1. --color-text-* is CONTENT-AREA ink. It must never appear inside the
        chrome (sidebar/topbar) — near-black on navy is invisible. The chrome
        has its own ink tokens.
     2. The accent has three tokens because no single orange can do all three
        jobs. Use them by role, not by taste — see the accent block below. */
:root {
  --sidebar-width: 240px;
  --topbar-height: 60px;
  --space-page: 28px;
  --form-page-width: 960px;

  --color-bg:          #eef1f6;
  --color-surface:     #ffffff;
  --color-surface-subtle: #f7f9fc;
  --color-surface-hover:  #f4f7fb;
  --color-border:      #d9e0ea;

  /* Neutral fills that used to be three copies of #eef2f7 */
  --color-table-head-bg: #f2f5f9;
  --color-row-hover-bg:  #f4f7fb;

  /* ── Chrome: the navy L formed by the sidebar and topbar ──
     Ratios are against --color-chrome-bg. */
  --color-chrome-bg:         #0c2047;
  --color-chrome-raised:     #152d5e;   /* topbar button hover; white on it 13.4:1 */
  --color-chrome-active:     #1b3a72;   /* active nav fill; white 11.1:1, accent-vivid bar 3.6:1 */
  --color-chrome-ink:        #ffffff;   /* 16.0:1 */
  --color-chrome-ink-muted:  #a9bcd9;   /*  8.3:1 — nav items, topbar icon buttons */
  --color-chrome-ink-subtle: #8fa5c8;   /*  6.4:1 — section labels, user role */
  --color-chrome-border:     rgba(255,255,255,.10);
  --color-chrome-hover-bg:   rgba(255,255,255,.07);

  /* The pre-existing sidebar/topbar names are kept as aliases so the nav-item,
     hover and active rules below need no edits. */
  --color-sidebar-bg:            var(--color-chrome-bg);
  --color-sidebar-text:          var(--color-chrome-ink-muted);
  --color-sidebar-hover-bg:      var(--color-chrome-hover-bg);
  --color-sidebar-hover-text:    var(--color-chrome-ink);
  --color-sidebar-active-bg:     var(--color-chrome-active);
  --color-sidebar-active-text:   var(--color-chrome-ink);
  --color-sidebar-active-accent: var(--color-accent-vivid);

  --color-topbar-bg:     var(--color-chrome-bg);
  --color-topbar-border: var(--color-chrome-border);

  /* Content ink — all pass WCAG AA (4.5:1) on white AND on --color-bg */
  --color-text-primary:   #0c2047;   /* 16.0:1 white · 14.1:1 page — the chrome navy, reused as ink */
  --color-text-secondary: #3d4a63;   /*  8.9:1 white ·  7.9:1 page */
  --color-text-muted:     #5f6d84;   /*  5.2:1 white ·  4.6:1 page
                                        (was #64748b, only 4.28:1 on the page bg — it never
                                         actually passed AA there despite the old comment) */

  /* ── Accent: three oranges, by role ──
     The brand orange under white text is only 3.09:1, so it cannot be a button
     fill. Splitting by role keeps the mobile app's colour where it shows and
     stays legible where it counts (same shape as --color-danger below). */
  --color-accent:       #c2500c;   /* solid fills carrying white text (4.72:1), borders, focus */
  --color-accent-ink:   #a8420a;   /* text and links on light surfaces: 6.09:1 white, 5.38:1 page,
                                      5.40:1 on --color-accent-light. Text on the tint MUST use
                                      this — --color-accent there is 4.19:1 and fails. */
  --color-accent-vivid: #ee6c1f;   /* the mobile app's orange: brand mark, active-nav bar, icon
                                      tiles, chart accent. ON WHITE (3.09:1) OR NAVY (5.18:1)
                                      ONLY — on --color-bg it drops to 2.73:1 and fails. */
  --color-accent-light: #fdefe3;   /* tint; pair with --color-accent-ink */
  --color-on-accent:    #ffffff;   /* ink on any accent/status fill */

  /* ── Status: reserved meanings. Never orange, never a chart series. ── */
  --color-success:       #0f7a45;   /* 5.40:1 on white; also fine as a fill under white text */
  --color-success-light: #e4f4ea;   /* --color-success on it: 4.74:1 */
  --color-warning:       #8f5606;   /* 6.00:1 on white */
  --color-warning-light: #fdf1dc;   /* --color-warning on it: 5.37:1 */

  --color-danger:       #c0322b;   /* 5.63:1 — borders and icons */
  --color-danger-light: #fbe7e5;
  --color-danger-bg:    #fffafa;
  --color-danger-strong:#b52d26;   /* 6.23:1 on white — for text and solid fills */

  /* Blue is otherwise unused outside chart series (see --chart-3/--chart-4 below,
     which are reserved for chart data and not a status colour). This is the Site
     Locations map marker's "no ongoing access" colour — the common, quiet case for
     most sites most of the time — the counterpart to the accent orange an *ongoing*
     access marker/card uses. No -light tint: the card for a quiet site stays plain
     white rather than a blue tint, so there's nothing to pair it with yet. */
  --color-info: #1d5fa8;   /* 6.45:1 on white; also fine as a fill under white text */

  --color-geofence: #6a3fa0;   /* 7.42:1 on white — the site-access detail page's sixth
     event-kind dot (core/site_access_events.py's GEOFENCE). Every other hue in that
     family is already spoken for (info/success/warning/danger, plus accent-vivid for
     ISSUE_REPORT), so this bucket needed one of its own rather than reusing a nearby
     orange and reading as the same kind. */

  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --radius-pill: 999px;

  --shadow-sm: 0 1px 2px rgba(12,32,71,.06), 0 1px 3px rgba(12,32,71,.08);
  --shadow-md: 0 6px 20px rgba(12,32,71,.14);

  --focus-ring:        0 0 0 3px rgba(194,80,12,.18);
  --focus-ring-danger: 0 0 0 3px rgba(181,45,38,.16);

  --font: 'Inter', system-ui, -apple-system, sans-serif;
  --font-display: 'Outfit', 'Inter', system-ui, sans-serif;
  /* Codes and IDs are set in Inter with tabular-nums rather than a mono face —
     the mobile app does the same, and the old 'JetBrains Mono' here was never
     loaded, so .text-mono has always fallen back to the system mono anyway. */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;

  --text-xs:   11.5px;
  --text-sm:   12.5px;
  --text-base: 14px;
  --text-md:   15px;
  --text-lg:   20px;
  --text-xl:   26px;

  /* ── Charts ──
     A single-hue sequential ramp, monotonic in OKLCH lightness
     (L .839 → .689 → .538 → .371), because the distributions it paints are
     ordered (duration bins, access-count bins) — categorical hues would hide
     that ordering. --chart-state is for a *state* outside the ordering
     ("ongoing"), which is why it leaves the ramp. Read by dashboard-charts.js
     via getComputedStyle, so this block is the single source of truth. */
  --chart-1: #b8cce6;
  --chart-2: #7e9dc8;
  --chart-3: #4a6fa5;
  --chart-4: #1c3f73;
  --chart-state: #a16207;
  /* The weekly chart's cumulative line. Warm against the cool bars, so the two
     series stay separable for colour-blind readers as well (the chart has no
     legend — bar-vs-line form is the only other cue). Currently the same value
     as --chart-state but a separate token on purpose: different meanings, and
     either should be able to move without dragging the other. */
  --chart-cumulative: #a16207;
  /* In-slice label ink flips at --chart-3: navy on 1-2 (9.8:1, 5.7:1),
     white on 3-4 and state (5.1:1, 10.5:1, 4.9:1). */
  --chart-label-on-light: #0c2047;
  --chart-label-on-dark:  #ffffff;
  --chart-grid: #e3e8f0;
  --chart-ink:  #5f6d84;           /* 5.24:1 on white */
}

/* Account Settings' Text size preference (uicore.context_processors.text_size puts
   the choice on <html data-text-size>). Rescales only the six --text-* tokens above —
   every rule in this file that reads one of them (`var(--text-…)`) follows for free,
   with no second override per component, and every `font-size` in this file *is* one of
   these tokens (see uicore/CLAUDE.md § Account settings for the retokenizing that made
   that true). What it does *not* reach: a `font-size` written as a literal px value in a
   page-specific stylesheet (home.css, site-form.css, …) rather than one of these tokens,
   and general spacing/control sizing (padding, `.form-field input`'s fixed height, icon
   dimensions, …) anywhere — neither is part of this preference, see that CLAUDE.md
   section for which is which and why. "Medium" needs no block of its own: it's the
   :root values already set above. */
:root[data-text-size='sm'] {
  --text-xs:   10px;
  --text-sm:   11px;
  --text-base: 12.5px;
  --text-md:   13.5px;
  --text-lg:   18px;
  --text-xl:   23px;
}

:root[data-text-size='lg'] {
  --text-xs:   13px;
  --text-sm:   14px;
  --text-base: 16px;
  --text-md:   17px;
  --text-lg:   23px;
  --text-xl:   30px;
}

/* ─── Reset & base ──────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

body {
  font-family: var(--font);
  font-size: var(--text-base);
  color: var(--color-text-primary);
  background: var(--color-bg);
  display: flex;
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; text-decoration: none; }
button { cursor: pointer; font-family: inherit; }

/* ─── Sidebar ───────────────────────────────────────────────────── */
/* Width is `max-content` down to a floor of `--sidebar-width`: the box hugs
   whichever nav label is longest (French/Spanish nav items routinely run
   longer than their English source — "Tags & Locations" vs. "Étiquettes et
   emplacements") rather than clipping or overlapping the page at a fixed
   240px. `position: sticky` + a bounded `height: 100vh` is what makes that
   possible with no JS: a `position: fixed` sidebar sits outside normal flow,
   so nothing would keep `.main-wrapper` in step with a width that changes per
   language. Sticky keeps it in flow — `.main-wrapper`'s own `flex: 1` absorbs
   whatever width the sidebar settles on — while still tracking the viewport
   as the page scrolls, the way `fixed` did. The bounded height is also what
   lets `.sidebar__nav`'s `overflow-y: auto` actually engage on a short
   viewport instead of pushing `.sidebar__footer` off the bottom of the
   screen with no way to reach it. */
.sidebar {
  width: max-content;
  min-width: var(--sidebar-width);
  height: 100vh;
  background: var(--color-sidebar-bg);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  z-index: 100;
  flex-shrink: 0;
  transition: transform .25s ease;
}

/* Deliberately no border-bottom here, and none on .sidebar's right edge: the
   sidebar and the topbar are the same navy, so with no seam at the 240px corner
   they read as one continuous L framing the workspace. */
.sidebar__brand {
  min-height: var(--topbar-height);
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 20px;
  flex-shrink: 0;
}

/* The full Phoenix Smartlocks lockup (mark + wordmark) as supplied, margins
   trimmed and nothing else — the wordmark is baked into these pixels rather
   than typeset, so there is no second font to keep in step with it. Height is
   fixed, width follows from the asset's own ~3:1 aspect via object-fit. */
.sidebar__brand-logo {
  height: 32px;
  width: auto;
  object-fit: contain;
  flex-shrink: 0;
}

/* A hairline rather than a filled rule: at this contrast (white on navy) a
   solid divider reads heavier than the mark it's meant to just separate from
   the product name. */
.sidebar__brand-divider {
  width: 1px;
  height: 28px;
  background: rgba(255, 255, 255, .22);
  flex-shrink: 0;
}

/* The product name, now living beside the company lockup rather than above a
   byline sentence — the logo already states the company. */
.sidebar__brand-amp {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 800;
  color: var(--color-chrome-ink);
  letter-spacing: .03em;
  white-space: nowrap;
}

/* Nav */
.sidebar__nav {
  flex: 1;
  /* Without this a column flex item's default min-height (its own content
     size) would never let it shrink below the nav's full height, so
     overflow-y: auto below could never actually trigger. */
  min-height: 0;
  overflow-y: auto;
  padding: 16px 12px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* The global scrollbar thumb below is a light neutral; on navy it would read as
   a bright stripe, so the sidebar's own scroller overrides it. */
.sidebar__nav::-webkit-scrollbar-thumb { background: rgba(255,255,255,.18); }
.sidebar__nav::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,.3); }

.nav-section {
  margin-bottom: 8px;
}

.nav-section__label {
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .09em;
  color: var(--color-chrome-ink-subtle);
  padding: 8px 8px 4px;
}

.nav-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  border-radius: var(--radius-sm);
  color: var(--color-sidebar-text);
  font-size: var(--text-base);
  font-weight: 500;
  transition: background .15s, color .15s;
  white-space: nowrap;
  position: relative;
}

.nav-item:hover {
  background: var(--color-sidebar-hover-bg);
  color: var(--color-sidebar-hover-text);
}

.nav-item.active {
  background: var(--color-sidebar-active-bg);
  color: var(--color-sidebar-active-text);
  font-weight: 600;
}

.nav-item.active::before {
  content: '';
  position: absolute;
  left: 0;
  top: 20%;
  height: 60%;
  width: 3px;
  background: var(--color-sidebar-active-accent);
  border-radius: 0 3px 3px 0;
}

.nav-item__icon {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  opacity: .85;
}

.nav-item:hover .nav-item__icon {
  opacity: 1;
}

/* The active row is the one place the brand orange appears in the nav, so the
   icon picks it up alongside the accent bar. */
.nav-item.active .nav-item__icon {
  opacity: 1;
  color: var(--color-accent-vivid);
}

/* Sidebar footer */
.sidebar__footer {
  position: relative;
  padding: 12px;
  border-top: 1px solid var(--color-chrome-border);
}

.sidebar-user {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 8px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background .15s;
}

.sidebar-user:hover { background: var(--color-sidebar-hover-bg); }

.sidebar-user__avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--color-accent-vivid);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-xs);
  font-weight: 700;
  color: var(--color-on-accent);
  flex-shrink: 0;
}

.sidebar-user__info { flex: 1; min-width: 0; }

.sidebar-user__name {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-chrome-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* A long account username isn't translated content and shouldn't grow the
     now-adaptive .sidebar the way a long nav label should — cap it to the
     floor width's own budget (avatar + dots + gaps + padding) so it keeps
     truncating with an ellipsis instead of contributing its full un-wrapped
     width to the sidebar's max-content sizing. 110px is that budget added up
     by hand (avatar 32 + dots ~24 + two 10px gaps + 24px footer padding +
     16px row padding) rather than read from a token, since none of those
     micro-spacings has one — if the avatar size or either padding changes,
     re-add this by hand too. */
  max-width: calc(var(--sidebar-width) - 110px);
}

.sidebar-user__role {
  font-size: var(--text-xs);
  color: var(--color-chrome-ink-subtle);
}

.sidebar-user__dots {
  color: var(--color-chrome-ink-subtle);
  flex-shrink: 0;
}

/* A white sheet floating on the navy — hence no border, which at this contrast
   would read as a bright halo. The shadow does the separating instead, and the
   item rules below stay on content-ink tokens because the surface is light. */
.sidebar-user-menu {
  display: none;
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: calc(100% + 6px);
  background: var(--color-surface);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  padding: 6px;
  z-index: 20;
}

.sidebar-user-menu--open { display: block; }

.sidebar-user-menu__item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 8px 10px;
  border: none;
  background: none;
  border-radius: var(--radius-sm);
  font-family: var(--font);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--color-text-primary);
  text-align: left;
  cursor: pointer;
  transition: background .15s;
}

.sidebar-user-menu__item:hover { background: var(--color-surface-hover); }

.sidebar-user-menu__item--danger { color: var(--color-danger-strong); }
.sidebar-user-menu__item--danger:hover { background: var(--color-danger-bg); }

.sidebar-user-menu__icon { flex-shrink: 0; }

/* ─── Main wrapper ──────────────────────────────────────────────── */
/* No margin-left offset: .sidebar is a sticky-positioned flex sibling now,
   not a fixed one pulled out of flow, so it already occupies real layout
   width and flex: 1 here absorbs whatever's left. */
.main-wrapper {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* ─── Topbar ────────────────────────────────────────────────────── */
/* Same navy as the sidebar, and no bottom border or shadow: the chrome should
   read as one shape, not two stacked bars. The light page background below it
   provides all the edge definition needed. */
.topbar {
  height: var(--topbar-height);
  background: var(--color-topbar-bg);
  display: flex;
  align-items: center;
  padding: 0 var(--space-page);
  gap: 16px;
  position: sticky;
  top: 0;
  z-index: 50;
}

.topbar__page-title {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 600;
  color: var(--color-chrome-ink);
  letter-spacing: .01em;
  flex: 1;
}

/* The wall clock at the right edge. Stacked, muted chrome ink, no fill or border: it
   should read as part of the bar rather than as a widget sitting on it. The time sits
   on top and carries full muted weight, with the zone beside it — the zone qualifies
   the time, not the date, so it stays on that line; the date sits a step back on the
   line below with no qualifier of its own. tabular-nums, because the time still
   renders AMP's one numeric format and without the tabular figures a ticking seconds
   digit reflows the row once a second. */
.topbar-clock {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  font-size: var(--text-xs);
  color: var(--color-chrome-ink-subtle);
  font-variant-numeric: tabular-nums;
  letter-spacing: .02em;
  white-space: nowrap;
}

/* `display: flex` above would otherwise beat the browser's default [hidden] rule,
   and the clock is hidden in the markup until topbar-clock.js has a reading. */
.topbar-clock[hidden] { display: none; }

.topbar-clock__time-row {
  display: flex;
  align-items: baseline;
  gap: 6px;
}

.topbar-clock__time {
  font-size: var(--text-sm);
  color: var(--color-chrome-ink-muted);
}


/* ─── Page content ──────────────────────────────────────────────── */
.page-content {
  flex: 1;
  padding: var(--space-page);
  overflow-y: auto;
}

/* ─── Scrollbar ─────────────────────────────────────────────────── */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--color-border); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: var(--color-text-muted); }

/* ─── List page ─────────────────────────────────────────────────── */
.list-page { display: flex; flex-direction: column; gap: 20px; }

.list-page__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.list-page__title {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: 700;
  color: var(--color-text-primary);
  letter-spacing: -.01em;
  line-height: 1.15;
}

.list-page__subtitle {
  font-size: var(--text-base);
  color: var(--color-text-muted);
  margin-top: 3px;
}

/* A short explanatory line above a list page's search bar — Admin Accounts' "these
   are not door-opening users" note and the access log's "all times shown in <zone>"
   note are the two callers, which is what makes this a shared token rather than a
   copy per page. */
.list-page__note {
  margin: 0 0 16px;
  max-width: 78ch;
  font-size: var(--text-sm);
  line-height: 1.55;
  color: var(--color-text-secondary);
}

/* ── Page header: icon tile + title + subtitle (_page_header.html) ──
   Shared by the list, form and confirm-delete pages. The tile is the one spot
   on a light page where the brand orange appears, which is legal because it sits
   on its own tint rather than on --color-bg. */
.page-header {
  display: flex;
  align-items: center;
  gap: 14px;
}

.page-header__tile {
  width: 46px;
  height: 46px;
  border-radius: var(--radius-lg);
  background: var(--color-accent-light);
  color: var(--color-accent-vivid);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.page-header__tile svg { width: 22px; height: 22px; }

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

.page-header__actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }

/* ─── Card ──────────────────────────────────────────────────────── */
.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
}

/* ─── Data table ────────────────────────────────────────────────── */
.data-table {
  width: 100%;
  border-collapse: collapse;
}

.data-table thead {
  background: var(--color-table-head-bg);
  border-bottom: 1px solid var(--color-border);
}

.data-table th {
  padding: 11px 20px;
  text-align: left;
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--color-text-muted);
  white-space: nowrap;
}

.data-table td {
  padding: 13px 20px;
  font-size: var(--text-base);
  color: var(--color-text-primary);
  border-bottom: 1px solid var(--color-border);
  vertical-align: middle;
  /* Counts, dates and durations line up column-wise without a mono face. */
  font-variant-numeric: tabular-nums;
}

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

.data-table tbody tr:hover td { background: var(--color-row-hover-bg); }

/* Trailing row-action column: shrink-to-fit, and vertically tighter than the
   default cell padding so a 28px icon button doesn't make rows taller. */
.data-table__actions { width: 1%; text-align: right; white-space: nowrap; }
.data-table td.data-table__actions { padding-top: 6px; padding-bottom: 6px; }
.data-table__actions .panel-row__delete { display: inline-flex; }

/* A row for an entity that is banned (people, companies). Three deliberate choices:
   the wash is --color-danger-bg rather than --color-danger-light so the .status--bad
   pill in the row still reads as a distinct object on top of it; the rail on the
   first cell is what makes a banned row findable while scrolling, since that wash
   alone is nearly white; and neither is ever the only cue — the pill beside it
   carries an icon and a word. inset box-shadow, not border-left, because a border
   would shift the cell's content by its own width on banned rows only. */
.data-table tbody tr.is-banned td { background: var(--color-danger-bg); }
.data-table tbody tr.is-banned td:first-child {
  box-shadow: inset 3px 0 0 var(--color-danger);
}
.data-table tbody tr.is-banned:hover td { background: var(--color-danger-light); }

/* The negative answer in the people and companies lists' Banned column. Deliberately
   quiet text and not a .status--good pill: it is the answer on nearly every row, so
   pilling it would put a badge on the whole table and bury the one exception the
   column exists to show — the same reason those rows carried nothing at all before
   the column started answering in both directions. */
.ban-no { color: var(--color-text-muted); }

.text-secondary { color: var(--color-text-secondary) !important; }

/* Serials, codes and IDs: Inter with tabular figures and a touch of tracking
   reads cleaner than a mono face at this size, and matches the mobile app. */
.text-mono {
  font-variant-numeric: tabular-nums;
  letter-spacing: .02em;
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
}

.code-badge {
  display: inline-flex;
  align-items: center;
  background: var(--color-accent-light);
  color: var(--color-accent-ink);
  font-variant-numeric: tabular-nums;
  letter-spacing: .02em;
  font-size: var(--text-sm);
  font-weight: 600;
  padding: 3px 9px;
  border-radius: 6px;
}

.code-badge--link:hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* A cross-reference in a table cell: the value stays the value, and clicking it
   opens the entity it names. Not .code-badge--link, which is the row's *own*
   identity in the first column — these cells hold names and codes belonging to
   other rows, and a second tinted pill per row would compete with it. Same
   treatment as the lock page's Site link (.lock-facts dd a), which is the same
   kind of link. --color-accent-ink is the text role of the accent (5.38:1 on the
   page bg); --color-accent would fail there. Underlined on hover/focus only,
   because an access-log row carries three of these and three permanently
   underlined cells make the table read as a nav menu.
   Note this wins over an ancestor .text-secondary despite its !important: that
   rule matches the <td>, and a declaration on the element itself beats an
   inherited value regardless of importance. */
.cell-link {
  color: var(--color-accent-ink);
  font-weight: 500;
}

.cell-link:hover,
.cell-link:focus-visible {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* ─── Button ────────────────────────────────────────────────────── */
/* Pills, matching the mobile app. Hover darkens the fill rather than fading it:
   opacity on an accent fill drops its contrast against white text below AA. */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 9px 18px;
  border-radius: var(--radius-pill);
  font-size: var(--text-base);
  font-weight: 600;
  border: none;
  cursor: pointer;
  transition: background .15s, box-shadow .15s, border-color .15s, color .15s;
  font-family: var(--font);
}

.btn:focus-visible { outline: none; box-shadow: var(--focus-ring); }

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

.btn--primary:hover {
  background: var(--color-accent-ink);
  box-shadow: 0 3px 10px rgba(168,66,10,.28);
}

/* ─── Empty state ───────────────────────────────────────────────── */
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 60px 24px;
  color: var(--color-text-muted);
}

/* Decorative only — the title beside it carries the message, so this sits below
   the 3:1 graphic threshold on purpose rather than competing with the text. */
.empty-state svg { width: 32px; height: 32px; color: #adbacd; stroke-width: 1.5; }

.empty-state__title {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 600;
  color: var(--color-text-secondary);
  margin-top: 4px;
}

.empty-state__sub { font-size: var(--text-sm); }

/* ─── Pagination ────────────────────────────────────────────────── */
.pagination {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 14px 20px;
  border-top: 1px solid var(--color-border);
}

.pagination__link,
.pagination__page {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 34px;
  height: 34px;
  padding: 0 9px;
  border-radius: var(--radius-pill);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--color-text-secondary);
  font-variant-numeric: tabular-nums;
}

.pagination__link:hover,
.pagination__page:hover { background: var(--color-surface-hover); }

.pagination__link--disabled { color: var(--color-text-muted); opacity: .45; cursor: default; }
.pagination__link--disabled:hover { background: none; }

.pagination__page--active {
  background: var(--color-accent-light);
  color: var(--color-accent-ink);
  font-weight: 600;
}

.pagination__ellipsis {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 34px;
  height: 34px;
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

/* ─── Form page ─────────────────────────────────────────────────────
   The only content-width cap in the app — .list-page and .dashboard are
   deliberately fluid. Forms are capped because their inputs are width:100%,
   so an uncapped page stretches "Country" or "Address line 1" to the full
   monitor. 960px is wide enough that a .form-fields--two-col pair reads as
   two real columns, and narrow enough that a lone text input keeps a sane
   measure. */
.form-page {
  display: flex;
  flex-direction: column;
  gap: 20px;
  max-width: var(--form-page-width);
}

.form-page__header { display: flex; flex-direction: column; gap: 4px; }

.back-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--color-text-muted);
  margin-bottom: 8px;
  transition: color .15s;
}

.back-link:hover { color: var(--color-accent-ink); }

.form-card { overflow: visible; }

.form-section {
  padding: 24px 24px 0;
}

.form-section__label {
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--color-text-muted);
  margin-bottom: 16px;
}

/* Sits between a section label and its fields, for a section whose *meaning* needs a
   sentence — the label's margin is dropped onto this instead so spacing is unchanged
   when it's absent. */
.form-section__label:has(+ .form-section__hint) { margin-bottom: 6px; }

.form-section__hint {
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--color-text-muted);
  margin: 0 0 16px;
  max-width: 62ch;
}

.form-fields {
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding-bottom: 24px;
}

.form-fields--two-col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 18px;
}

/* The sidebar is --sidebar-width (240px) or wider — it grows with the active
   language's nav labels — so the content column is at most viewport − 296px.
   Below ~780px that leaves under 480px even at the floor width, at which
   point two inputs side by side are narrower than the text they hold. */
@media (max-width: 780px) {
  .form-fields--two-col { grid-template-columns: 1fr; }
}

.form-divider {
  height: 1px;
  background: var(--color-border);
  margin: 0 24px;
}

.form-field { display: flex; flex-direction: column; gap: 5px; }

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

.form-label__required { color: var(--color-danger); margin-left: 2px; }

.form-field input {
  height: 40px;
  padding: 0 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-size: var(--text-base);
  font-family: var(--font);
  color: var(--color-text-primary);
  background: var(--color-surface);
  outline: none;
  transition: border-color .2s, box-shadow .2s;
  width: 100%;
}

/* Controls are width:100%, so on a single-column section they would run the
   full --form-page-width — a 960px box holding a short company ID. 32rem is
   under the ~28rem a .form-fields--two-col column gets at full page width, so
   this only ever bites in single-column sections and never desyncs a pair.
   Scoped to .form-field on purpose: the tag picker and lock search inputs are
   .tag-add-select (flex children that must stretch), and _hour_grid.html
   deliberately bypasses _form_field.html so its 7×24 grid is untouched. */
.form-field input,
.form-field select {
  max-width: 32rem;
}

.form-field input:focus,
.form-field select:focus {
  border-color: var(--color-accent);
  box-shadow: var(--focus-ring);
}

.form-field input::placeholder { color: var(--color-text-muted); }

/* The chevron is a data-URI, so its stroke cannot be a var() — it is
   --color-text-muted (#5f6d84) hand-encoded as %235f6d84. Keep the two in sync. */
.form-field select {
  height: 40px;
  padding: 0 32px 0 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-size: var(--text-base);
  font-family: var(--font);
  color: var(--color-text-primary);
  background: var(--color-surface) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%235f6d84' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E") no-repeat right 12px center;
  appearance: none;
  outline: none;
  transition: border-color .2s, box-shadow .2s;
  width: 100%;
  cursor: pointer;
}

.form-field--error input,
.form-field--error select {
  border-color: var(--color-danger);
  background-color: var(--color-danger-bg);
}

.form-field--error input:focus,
.form-field--error select:focus {
  box-shadow: var(--focus-ring-danger);
}

.form-hint {
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}

.form-error {
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--color-danger-strong);
}

/* ── Boolean field as a switch row (_form_switch.html) ───────
   A framed row rather than a bare checkbox: the flags rendered this way revoke
   access, so they get the visual weight of a decision instead of the weight of a
   preference, and the frame tints while the flag is on so the state survives a
   glance at a long form. Note it lives outside .form-field on purpose — that
   block's `input { height: 40px; width: 100% }` would inflate the checkbox. */
.form-switch {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  max-width: 32rem;              /* matches .form-field input/select */
  padding: 14px 16px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface-subtle);
  transition: background .2s, border-color .2s;
}

.form-switch--on {
  border-color: var(--color-danger);
  background: var(--color-danger-bg);
}

.form-switch input[type="checkbox"] {
  width: 17px;
  height: 17px;
  margin-top: 1px;               /* optical alignment with the label's cap height */
  flex-shrink: 0;
  accent-color: var(--color-danger-strong);
  cursor: pointer;
}

.form-switch input[type="checkbox"]:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring-danger);
  border-radius: 3px;
}

/* The switch was built for `is_banned`, where on means revoked — so danger is the tint,
   the accent and the focus ring. `is_active` on an admin account is the same control
   inverted: on is the ordinary state and off is the exception. Reusing the default there
   painted a perfectly healthy account in warning red, so this modifier swaps the three
   danger references for success ones rather than adding a second switch component.
   Pass tone="good" to _form_switch.html.

   Placed *after* the base rules on purpose: these selectors have the same specificity as
   the ones they override, so source order is the only thing deciding, and moving this
   block up silently restores the red checkbox while leaving the green frame in place. */
.form-switch--good.form-switch--on {
  border-color: var(--color-success);
  background: var(--color-success-light);
}

.form-switch--good input[type="checkbox"] {
  accent-color: var(--color-success);
}

.form-switch--good input[type="checkbox"]:focus-visible {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-success) 30%, transparent);
}

.form-switch__text { display: flex; flex-direction: column; gap: 4px; }

.form-switch__label {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--color-text-primary);
  cursor: pointer;
}

/* State the switch itself doesn't control — a person inheriting a company ban.
   Danger ink rather than --color-text-muted: it is the reason the unticked box
   above it doesn't mean what it appears to mean. */
.form-switch__note {
  display: flex;
  align-items: flex-start;   /* the note wraps to two lines; centring floats the icon */
  gap: 6px;
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--color-danger-strong);
}

.form-switch__note svg { flex-shrink: 0; margin-top: 2px; }

/* A rejected tag set (e.g. an access rule with no lock tags). Sits under the section
   label, above the chips, where _form_field.html would put a field error. */
.tag-picker__error {
  margin: 0 0 14px;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--color-danger-strong);
}

/* ── Inline tag widget (chip + dropdown) ─────────────────── */
/* Attribute rather than an id: a form may carry several pickers (see
   _tag_chip_picker.html), and ids would collide. */
[data-tag-hidden] { display: contents; }

/* The picker owns its internal spacing rather than inheriting .form-fields' gap, because
   it is no longer always inside one — _site_locks_panel.html includes the chrome-free
   partial per lock row. Same 18px, so the form pages are unchanged; the panel narrows it
   with a descendant rule in site-form.css. */
[data-tag-picker] {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.tag-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  min-height: 0;
}

.tag-chips:not(:empty) { margin-bottom: 10px; }

.tag-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 9px 5px 13px;
  background: var(--color-accent-light);
  color: var(--color-accent-ink);
  border-radius: var(--radius-pill);
  font-size: var(--text-sm);
  font-weight: 500;
}

.tag-chip__remove {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  color: inherit;
  opacity: 0.55;
  border-radius: 50%;
  transition: opacity .15s, background .15s;
}

.tag-chip__remove:hover {
  opacity: 1;
  background: rgba(168,66,10,.14);
}

.tag-add-row {
  display: flex;
  gap: 8px;
  align-items: center;
}

.tag-add-select {
  height: 36px;
  padding: 0 10px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-family: var(--font);
  color: var(--color-text-primary);
  background: var(--color-surface);
  outline: none;
  transition: border-color .2s, box-shadow .2s;
}

/* Scoped to select: _site_locks_panel.html also puts .tag-add-select on a text
   input, which must not get a chevron. Mirrors .form-field select, including the
   hand-encoded --color-text-muted stroke. */
select.tag-add-select {
  padding-right: 30px;
  appearance: none;
  cursor: pointer;
  background: var(--color-surface) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%235f6d84' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E") no-repeat right 10px center;
}

.tag-add-select:focus {
  border-color: var(--color-accent);
  box-shadow: var(--focus-ring);
}

.tag-empty-hint {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

.form-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  padding: 16px 24px;
  border-top: 1px solid var(--color-border);
  background: var(--color-surface-subtle);
}

/* Pushes a destructive action to the opposite end of the footer from Cancel/Save. */
.form-actions__destructive { margin-right: auto; }

/* A caveat printed under the form card rather than inside it — currently the
   "changes reach clients at their next refresh" line the access-rule and lock forms
   both carry. Shared here because it's a form-page primitive, not either page's
   own: it arrived with access-rules.css and moved when the second caller appeared. */
.form-page__footnote {
  margin: 14px 2px 0;
  font-size: var(--text-xs);
  line-height: 1.55;
  color: var(--color-text-muted);
}

.btn--ghost {
  background: transparent;
  color: var(--color-text-secondary);
  border: 1px solid var(--color-border);
}

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

.btn--sm {
  padding: 7px 15px;
  font-size: var(--text-sm);
  white-space: nowrap;
  flex-shrink: 0;
}

/* The button that actually commits a destructive action. */
.btn--danger {
  background: var(--color-danger-strong);
  color: var(--color-on-accent);
}

.btn--danger:hover {
  background: #9e251f;
  box-shadow: 0 3px 10px rgba(181,45,38,.28);
}

.btn--danger:focus-visible { box-shadow: var(--focus-ring-danger); }

/* A destructive action on a page whose primary action is something else (e.g.
   Save on the entity edit pages) — must not out-shout .btn--primary. */
.btn--danger-ghost {
  background: transparent;
  color: var(--color-danger-strong);
  border: 1px solid var(--color-danger-light);
}

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

/* ── Panel: a card of rows with a header and an action footer ────
   Shared by the Tags page panels (_tag_panel.html) and the Locks panel on
   the site edit page (_site_locks_panel.html). Row *contents* are per-panel;
   only the chrome lives here. */
.panel__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 20px;
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface-subtle);
}

.panel__title-row {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--color-text-secondary);
}

.panel__title {
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--color-text-muted);
}

.panel__count {
  font-size: var(--text-xs);
  font-weight: 700;
  background: var(--color-accent-light);
  color: var(--color-accent-ink);
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  font-variant-numeric: tabular-nums;
}

.panel__list { padding: 8px 0; min-height: 48px; }

.panel__empty {
  padding: 20px;
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  text-align: center;
}

.panel__footer {
  padding: 12px 16px;
  border-top: 1px solid var(--color-border);
  background: var(--color-surface-subtle);
}

.panel-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 20px;
  gap: 12px;
  border-bottom: 1px solid var(--color-border);
  transition: background .12s;
}

.panel-row:last-child { border-bottom: none; }
.panel-row:hover { background: var(--color-surface-hover); }

.panel-row__action { flex-shrink: 0; }

.panel-row__delete {
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  color: var(--color-text-muted);
  border-radius: var(--radius-sm);
  transition: background .12s, color .12s;
}

.panel-row__delete:hover {
  background: var(--color-danger-light);
  color: var(--color-danger-strong);
}

/* ─── Status pill ────────────────────────────────────────────────────
   Reserved meanings, one per token family. Always rendered with an icon and a
   text label beside the colour, never colour alone. */
.status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 11px 4px 8px;
  border-radius: var(--radius-pill);
  font-size: var(--text-sm);
  font-weight: 600;
}

.status svg { width: 14px; height: 14px; flex-shrink: 0; }

.status--good { background: var(--color-success-light); color: var(--color-success); }
.status--warn { background: var(--color-warning-light); color: var(--color-warning); }
.status--bad  { background: var(--color-danger-light);  color: var(--color-danger-strong); }

/* ─── Toasts ─────────────────────────────────────────────────────────
   django.contrib.messages, rendered once in base.html so a toast set before a
   redirect (e.g. client_app_settings on save) still shows up wherever that
   redirect lands. Fixed to the viewport corner, above the sidebar's z-index
   so it can never be hidden behind chrome. */
.toast-stack {
  position: fixed;
  bottom: 24px;
  right: 24px;
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: 360px;
}

.toast {
  background: var(--color-chrome-bg);
  color: var(--color-chrome-ink);
  padding: 12px 18px;
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  font-size: var(--text-sm);
  border-left: 4px solid var(--color-success);
  animation: toast-in .2s ease;
}

.toast--error   { border-left-color: var(--color-danger-strong); }
.toast--warning { border-left-color: var(--color-warning); }

.toast--hide { animation: toast-out .2s ease forwards; }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes toast-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}

/* ─── Read-only facts (see _entity_facts.html) ──────────────────────────────
 *
 * What a form page looks like to someone who may read it but not change it: the same
 * .form-section chrome, with a <dl> of values where the inputs would be. Shared by the
 * site / company / person pages and the KLA settings page, which is why it lives here
 * rather than in any one of their stylesheets.
 *
 * Deliberately mirrors the editable page's stacked full-width rhythm rather than
 * compressing into a grid — one row per field, label above value, hint below — so the
 * two renderings of the same page read as the same page. */
.facts {
  display: flex;
  flex-direction: column;
  gap: 18px;
  margin: 0;
  padding-bottom: 4px;
}

.facts__item {
  min-width: 0;
  padding-left: 12px;
  border-left: 2px solid var(--color-border);
}

/* On the editable page, the input's own border/background separates .form-label from
 * its value even though both are 13px/500 — a distinction this <dl> has no box to
 * borrow, so dt/dd need their own contrast instead of copying those two rules. */
.facts dt {
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--color-text-muted);
  margin-bottom: 4px;
}

.facts dd {
  margin: 0;
  font-size: var(--text-md);
  font-weight: 600;
  color: var(--color-text-primary);
}

/* The help text under the value. Muted and smaller so the value stays the thing the
 * eye lands on — the same relationship .form-hint has to its input. */
.facts__hint {
  margin: 4px 0 0;
  font-size: var(--text-xs);
  line-height: 1.55;
  color: var(--color-text-muted);
}

/* State this row is in that the value above cannot explain by itself — a person who is
 * not banned but whose company is. Danger ink and the same ban icon as
 * .form-switch__note, which is the editable page's version of exactly this line. */
.facts__note {
  display: flex;
  align-items: flex-start;   /* the note wraps; centring floats the icon */
  gap: 6px;
  margin: 4px 0 0;
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--color-danger-strong);
}

.facts__note svg { flex-shrink: 0; margin-top: 2px; }

/* The em dash standing in for an empty section (a page with no tags). Muted to the same
 * weight as a hint: it is the absence of content, not content. */
.facts__empty {
  margin: 0;
  font-size: var(--text-base);
  color: var(--color-text-muted);
}

/* A tag chip with nothing to remove. The picker's chips reserve their right padding for
 * a remove button; a read-only one has none, so it gets its padding back. */
.tag-chip--static { padding-right: 13px; }
