/* components.css — structural, theme-agnostic UI components.
 *
 * Phase 1 of the UI shared-asset refactor. Every rule references only
 * var(--*) tokens so it renders correctly under either token file
 * (tokens-admin.css dark-neon / tokens-portal.css light).
 *
 * The inline <style> duplicates have been removed from both base.html files.
 * Four previously theme-divergent rules are now tokenized:
 *   a:hover             → var(--link-hover)
 *   .topnav background  → var(--nav-bg)
 *   .btn-primary fg     → var(--btn-primary-fg)
 *   .btn-primary hover  → var(--btn-primary-hover-bg)
 *   .btn-danger hover   → var(--btn-danger-hover-fg)
 * Per-theme values live in tokens-admin.css / tokens-portal.css.
 *
 * Admin-only identity CSS (body::before/::after circuit grid, --glow-*,
 * --accent-2) is NOT here — it stays inline in admin base.html.
 */

/* ── Reset ───────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }

body {
  font-family: var(--font-body);
  background: var(--bg);
  color: var(--text);
  min-height: 100vh;
  font-size: 13.5px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

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

/* ── Nav ─────────────────────────────────────────────────────────── */
/* Measured at 390px before this change: the bar was 420px wide inside a 390px
 * viewport, so the whole DOCUMENT scrolled sideways and "Log out" sat off the
 * right edge. Two causes, both fixed here:
 *   - `padding: 0 2rem` spent 64px of a 390px screen on side padding
 *   - `height: 52px` with no wrap meant the brand, which wraps to two lines at
 *     that width, overflowed its own bar
 * min-height keeps the 52px bar on desktop; height is free to grow when the
 * content genuinely needs a second row. */
.topnav {
  position: sticky; top: 0; z-index: 300;
  min-height: 52px;
  padding: 0.5rem 1rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem;
  background: var(--nav-bg);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border-bottom: 1px solid var(--border);
}

@media (min-width: 681px) {
  .topnav { padding: 0 2rem; }
}
/* min-width:0 so a flex child may shrink below its content width — without it
 * the brand pushes the bar wider than the viewport no matter the padding. */
.nav-brand {
  font-family: var(--font-hd);
  font-size: 1.1rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text);
  display: flex; align-items: center; gap: 10px;
  min-width: 0;
  text-decoration: none;
}
.nav-brand:hover { color: var(--text); }
.nav-hex {
  width: 20px; height: 20px; flex-shrink: 0;
  background: var(--accent);
  clip-path: polygon(50% 0%,95% 25%,95% 75%,50% 100%,5% 75%,5% 25%);
}
.nav-right {
  display: flex; align-items: center; gap: 0.75rem;
  min-width: 0;
  flex-wrap: wrap;
  justify-content: flex-end;
}
/* .nav-user (the bare-email pill) was deleted 2026-08-28: both surfaces now
   show identity through the .nav-account menu trigger instead. */

/* ── Main ────────────────────────────────────────────────────────── */
.pg {
  position: relative; z-index: 1;
  max-width: 1080px;
  margin: 0 auto;
  padding: 2.25rem 2rem;
}
.pg > * {
  animation: fadein 0.32s cubic-bezier(0.22,1,0.36,1) both;
}
.pg > *:nth-child(1) { animation-delay: 0ms; }
.pg > *:nth-child(2) { animation-delay: 55ms; }
.pg > *:nth-child(3) { animation-delay: 100ms; }
.pg > *:nth-child(4) { animation-delay: 140ms; }
.pg > *:nth-child(n+5) { animation-delay: 170ms; }
@keyframes fadein {
  from { opacity: 0; transform: translateY(7px); }
  to   { opacity: 1; transform: none; }
}

/* ── Typography ──────────────────────────────────────────────────── */
h1 {
  font-family: var(--font-hd);
  font-size: 1.6rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text);
  line-height: 1.1;
}
h2 {
  font-family: var(--font-hd);
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text);
}

/* ── Card ────────────────────────────────────────────────────────── */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: 1.25rem 1.5rem;
  margin-bottom: 0.75rem;
  transition: border-color var(--ease), box-shadow var(--ease);
}
.card:hover { border-color: var(--border-2); }
.card-header {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 0.875rem;
}

/* ── Badges ──────────────────────────────────────────────────────── */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 8px;
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: 0.65rem;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}
.badge::before {
  content: '';
  width: 5px; height: 5px;
  border-radius: 50%;
  flex-shrink: 0;
}
.badge-success { background: var(--success-bg); color: var(--success); border: 1px solid var(--success-br); }
.badge-success::before { background: var(--success); }
.badge-error   { background: var(--error-bg);   color: var(--error);   border: 1px solid var(--error-br);   }
.badge-error::before   { background: var(--error); }
.badge-running { background: var(--running-bg); color: var(--running); border: 1px solid var(--running-br); }
.badge-running::before { background: var(--running); animation: blink 1.3s ease-in-out infinite; }
.badge-never   { background: var(--never-bg);   color: var(--text-3);  border: 1px solid var(--never-br);   }
.badge-never::before   { background: var(--text-3); }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.15} }

/* ── Source chips ────────────────────────────────────────────────── */
.source-grid { display: flex; flex-wrap: wrap; gap: 6px; }
.source-chip {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 3px 10px;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  color: var(--text-2);
  display: flex; align-items: center; gap: 7px;
  transition: border-color var(--ease);
}
.source-chip:hover { border-color: var(--border-2); }

/* ── Buttons ─────────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 14px;
  border-radius: var(--r-sm);
  font-family: var(--font-hd);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer; border: none;
  transition: all var(--ease);
  white-space: nowrap;
}
.btn-primary { background: var(--accent); color: var(--btn-primary-fg); }
.btn-primary:hover { background: var(--btn-primary-hover-bg); color: var(--btn-primary-fg); }
.btn-ghost   { background: transparent; color: var(--text-2); border: 1px solid var(--border); }
.btn-ghost:hover { border-color: var(--border-2); color: var(--text); }

/* ── Utilities ───────────────────────────────────────────────────── */
.section-label {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-bottom: 0.5rem;
}
.divider {
  border: none;
  border-top: 1px solid var(--border);
  margin: 1rem 0;
}
.breadcrumb {
  display: flex; align-items: center; gap: 7px;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  color: var(--text-3);
  margin-bottom: 1.5rem;
}
.breadcrumb a { color: var(--text-3); }
.breadcrumb a:hover { color: var(--text-2); }

/* ── Notices / flash ─────────────────────────────────────────────── */
.notice {
  padding: 8px 12px;
  border-radius: var(--r-sm);
  font-size: 0.78rem;
  margin-bottom: 0.75rem;
}
.notice-success { background: var(--success-bg); border: 1px solid var(--success-br); color: var(--success); }
.notice-error   { background: var(--error-bg);   border: 1px solid var(--error-br);   color: var(--error);   }

/* ── Danger / secondary buttons ──────────────────────────────────── */
.btn-danger {
  padding: 4px 10px;
  border-radius: var(--r-sm);
  border: 1px solid var(--error-br);
  background: var(--error-bg);
  color: var(--error);
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-weight: 500;
  cursor: pointer;
  transition: all var(--ease);
}
.btn-danger:hover:not(:disabled) { background: var(--error); border-color: var(--error); color: var(--btn-danger-hover-fg); }
.btn-danger:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-danger:focus-visible { outline: 2px solid var(--error); outline-offset: 2px; }

.btn-secondary {
  padding: 4px 10px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border-2);
  background: var(--surface);
  color: var(--text-2);
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-weight: 500;
  cursor: pointer;
  transition: all var(--ease);
}
.btn-secondary:hover { border-color: var(--accent); color: var(--accent); }
.btn-secondary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── Form field patterns ─────────────────────────────────────────── */
.form-input {
  padding: 7px 11px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border-2);
  background: var(--surface);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 0.84rem;
  transition: border-color var(--ease), box-shadow var(--ease);
}
.form-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }

.form-select {
  padding: 7px 11px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border-2);
  background: var(--surface);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 0.84rem;
  cursor: pointer;
}

.field { display: flex; flex-direction: column; gap: 5px; margin-bottom: 1.1rem; }
.field label {
  font-family: var(--font-mono);
  font-size: 0.67rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-3);
}
.field input {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 8px 12px;
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 0.82rem;
  outline: none;
  transition: border-color var(--ease), box-shadow var(--ease);
  width: 100%;
}
.field select {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 8px 12px;
  color: var(--text);
  font-family: var(--font-body);
  font-size: 0.82rem;
  outline: none;
  width: 100%;
  min-width: 0;
}
.field input:focus, .field select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
.field input::placeholder { color: var(--text-3); }
.field-hint { font-family: var(--font-mono); font-size: 0.62rem; color: var(--text-3); }

.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }

/* Wraps: .btn is white-space:nowrap, so three actions ("Save Changes" /
 * "Web Pixel" / "Cancel" on rooftop_edit) pushed the page to 405px at 390. */
.form-actions {
  display: flex; align-items: center; gap: 0.75rem;
  flex-wrap: wrap;
  margin-top: 1.25rem; padding-top: 1.1rem;
  border-top: 1px solid var(--border);
}

.form-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: 1.5rem 1.75rem 1.5rem;
  max-width: 560px;
}
.form-card h2 {
  margin-bottom: 1.25rem;
  padding-bottom: 0.625rem;
  border-bottom: 1px solid var(--border);
}
.form-section-label {
  font-family: var(--font-mono);
  font-size: 0.65rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-3);
  margin-bottom: 1rem;
  margin-top: 1.5rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--border);
}
.form-section-label:first-of-type { margin-top: 0; }

/* ── Empty state ─────────────────────────────────────────────────── */
.empty-state {
  border: 1px dashed var(--border-2);
  border-radius: var(--r);
  padding: 3.5rem;
  text-align: center;
  font-size: 0.825rem;
  color: var(--text-3);
}

/* ── Data tables ─────────────────────────────────────────────────── */
/*
 * Base recipe: portal-style body-font tables (team, loc, sim, notes).
 * font-size 0.82rem, td hover → --surface-2, no forced td bg.
 *
 * Modifiers:
 *   .data-table--mono   admin mono-font tables (users, detail, rooftop, inv-catalog)
 *                       font-mono, td bg → --bg, hover → --surface
 *   .data-table--compact  portal inv-table style: 0.8rem, smaller padding, top-align
 *   .data-table--wrap   wrapping container (border + overflow:hidden + border-radius)
 */
.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.82rem;
}
.data-table th {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-3);
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--border);
  text-align: left;
  white-space: nowrap;
}
.data-table td {
  padding: 0.75rem;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}
.data-table tr:last-child td { border-bottom: none; }
.data-table tr:hover td { background: var(--surface-2); }

/* Mono variant — admin tables with font-mono, --bg base, hover → --surface */
.data-table--mono {
  font-family: var(--font-mono);
  font-size: 0.75rem;
}
.data-table--mono thead tr {
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
}
.data-table--mono th {
  padding: 8px 14px;
  font-size: 0.62rem;
}
.data-table--mono td {
  padding: 8px 14px;
  background: var(--bg);
  color: var(--text-2);
  vertical-align: middle;
  transition: background var(--ease);
}
.data-table--mono tr:hover td { background: var(--surface); }

/* Compact variant — portal inventory tables, 0.8rem, tighter padding, top-align */
.data-table--compact {
  font-size: 0.8rem;
}
.data-table--compact th {
  font-size: 0.62rem;
  padding: 6px 10px;
}
.data-table--compact td {
  padding: 8px 10px;
  vertical-align: top;
}

/* Notes variant — memory.html: 6px/12px th padding, 10px/12px td, top-align */
.data-table--notes th {
  padding: 6px 12px;
  font-size: 0.63rem;
}
.data-table--notes td {
  padding: 10px 12px;
  vertical-align: top;
}

/* Wrap container — border + rounded, and the table SCROLLS inside it.
 *
 * This was `overflow: hidden`, which does not hide a scrollbar — it destroys
 * access to the content. Measured at 390px: 525px of table inside a 324px
 * container, so Rooftop / Last seen / Status were not behind a swipe, they were
 * unreachable. overflow-x: auto keeps the rounded corners clipped and gives the
 * columns back. */
.data-table--wrap {
  border-radius: var(--r);
  border: 1px solid var(--border);
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  margin-top: 1.25rem;
}

/* Files-table selected-row state (files_dashboard only) */
.data-table tr.selected td { background: var(--accent-dim); }

/* ── File-badge extensions ───────────────────────────────────────────── */
/*
 * Shared by admin (files_dashboard) and portal (portal_files).
 * Requires --warning / --warning-bg / --warning-br tokens (both token files).
 * Status badges mirror the status_badge() macro in _shared/components.html.
 */
.badge-downloading { background: var(--running-bg); color: var(--running); border: 1px solid var(--running-br); }
.badge-downloading::before { background: var(--running); animation: blink 1.3s ease-in-out infinite; }
.badge-deleted     { background: var(--error-bg);   color: var(--error);   border: 1px solid var(--error-br); }
.badge-deleted::before     { background: var(--error); }
.badge-pending     { background: var(--never-bg);   color: var(--text-3);  border: 1px solid var(--never-br); }
.badge-pending::before     { background: var(--text-3); }
.badge-parsing     { background: var(--running-bg); color: var(--running); border: 1px solid var(--running-br); }
.badge-parsing::before     { background: var(--running); animation: blink 1.3s ease-in-out infinite; }
.badge-parsed      { background: var(--success-bg); color: var(--success); border: 1px solid var(--success-br); }
.badge-parsed::before      { background: var(--success); }
.badge-failed      { background: var(--error-bg);   color: var(--error);   border: 1px solid var(--error-br); }
.badge-failed::before      { background: var(--error); }

/* Needs-review badge */
.badge-review {
  background: var(--warning-bg); color: var(--warning); border: 1px solid var(--warning-br);
  font-weight: 700;
}
.badge-review::before { background: var(--warning); }

/* Sensitivity badges */
.badge-sensitivity-public {
  background: var(--success-bg); color: var(--success); border: 1px solid var(--success-br);
}
.badge-sensitivity-public::before { background: var(--success); }
.badge-sensitivity-internal {
  background: var(--never-bg); color: var(--text-3); border: 1px solid var(--never-br);
}
.badge-sensitivity-internal::before { background: var(--text-3); }
.badge-sensitivity-restricted {
  background: rgba(224,64,251,0.10); color: #e040fb; border: 1px solid rgba(224,64,251,0.30);
  font-weight: 700;
}
.badge-sensitivity-restricted::before { background: #e040fb; }

/* ── Account page components ─────────────────────────────────────── */
/*
 * Shared between admin/account_detail.html and portal/account.html.
 * Every value references var(--*) so dark-neon (admin) and light (portal)
 * themes are handled purely by which token file loads.
 *
 * Rules that could NOT be shared without visual change are left inline:
 *   - .src-icon-btn  (admin=ghost/border-2/text-3 default; portal=filled/accent-dim)
 *   - .src-tile cursor (admin adds cursor:default; portal omits it)
 *   - .err-chip* / .err-drawer* (admin-only, kept inline in account_detail.html)
 *   - .inv-* invite-section classes (admin-only, kept inline in account_detail.html)
 *   - Responsive media queries (kept in each file; identical markup so safe)
 */

/* ── Page header ── */
.pg-header {
  display: flex;
  /* Wrap, or the action buttons overflow the viewport instead of moving to
     their own line. Measured on /admin/accounts/{id} at 768px: the six-button
     row was 686px wide and pushed the document to 888px, so the page scrolled
     sideways on an iPad in portrait. The existing max-width:680 rule could not
     help — 768 is above it, and .pg-header-actions carries flex-shrink:0, so
     nothing gave. Wrapping only engages when the content genuinely does not
     fit, so wide screens are unaffected. */
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  padding-bottom: 1.5rem;
  border-bottom: 1px solid var(--border);
  margin-bottom: 1.5rem;
}
.pg-header-left { display: flex; flex-direction: column; gap: 4px; }
.pg-eyebrow {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--cyan);
}
.pg-header-actions {
  display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0;
  padding-top: 4px;
  /* Wrap among themselves too: six buttons never fit one line on a phone. */
  flex-wrap: wrap;
}

/* ── Stat bar ── */
.stat-bar {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0.625rem;
  margin-bottom: 1.75rem;
}
.stat-tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: 0.875rem 1.125rem;
  display: flex; flex-direction: column; gap: 4px;
  transition: border-color var(--ease);
}
.stat-tile:hover { border-color: var(--border-2); }
.stat-label {
  font-family: var(--font-mono);
  font-size: 0.58rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-3);
}
.stat-value {
  font-family: var(--font-display, var(--font-hd));
  font-size: 1.45rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--text);
  line-height: 1;
}
.stat-value.has-error { color: var(--error); }
.stat-value.has-ok    { color: var(--success); }
.stat-sub {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  color: var(--text-3);
}

/* ── Section heading ── */
.section-hd {
  display: flex; align-items: center; justify-content: space-between;
  /* Several of these carry a form beside the heading (the invite row on the
     account page), and without wrapping the pair overflowed a 320px viewport.
     Wrapping engages only when the content does not fit. */
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 0.875rem;
}
.section-hd h2 {
  font-family: var(--font-display, var(--font-hd));
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-3);
  margin: 0;
}

/* ── Source grid ── */
/* minmax(0, ...) everywhere, not 1fr.
 *
 * `1fr` means `minmax(auto, 1fr)`, and `auto` floors the track at its items'
 * MIN-CONTENT width. A tile whose footer holds two nowrap buttons therefore
 * forces the track — and the page — wider than the viewport. That is what
 * pushed the source cards off the right edge on a phone. minmax(0, ...) lets
 * the track be as narrow as the container and makes the tile shrink instead.
 *
 * This cannot be verified by matching a font metric: the buttons are 0.60rem
 * --font-mono, which measures narrower in headless Chromium than on real iOS,
 * so the overflow reproduces on device and not in the harness. The fix is
 * therefore structural — the track can no longer be forced wide by ANY content
 * width — and the test below uses a deliberately hostile fixture rather than
 * trying to imitate iOS. */
.src-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(220px, 100%), 1fr));
  gap: 0.625rem;
  margin-bottom: 2rem;
}
.src-tile {
  min-width: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: 0.875rem 1rem;
  display: flex; flex-direction: column; gap: 0.625rem;
  transition: border-color var(--ease), box-shadow var(--ease), transform var(--ease);
  position: relative;
}
.src-tile:hover {
  border-color: var(--border-2);
  transform: translateY(-1px);
  box-shadow: var(--tile-hover-shadow);
}
.src-tile.has-error { border-color: var(--error-br); }
.src-tile.has-error:hover {
  border-color: var(--error);
  box-shadow: var(--tile-error-hover-shadow);
}
.src-tile-top {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
}
.src-tile-label {
  font-family: var(--font-display, var(--font-hd));
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text);
}
/* min-width:0 or the ellipsis never engages: a flex item defaults to
 * min-width:auto, i.e. min-content, so `text-overflow` has nothing to clip. */
.src-tile-name {
  min-width: 0;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  color: var(--text-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Status lines (an error, "No new data", "Last synced") must be read in full:
 * the ellipsis above cut Google's error sentence mid-word on a 390px phone and
 * the full text sat only in a hover title nobody on a phone can see. Wrap
 * anywhere, because an error can be one long token. */
.src-tile-name.src-tile-note,
.src-tile-ts.src-tile-note {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  overflow-wrap: anywhere;
}
.src-tile-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 0.375rem;
  flex-wrap: wrap;
  margin-top: auto;
}
.src-tile-ts {
  min-width: 0;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  color: var(--text-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* flex-shrink was 0, which is what let two nowrap buttons dictate the tile's
 * minimum width. They wrap now instead. */
.src-tile-actions {
  display: flex; align-items: center; gap: 4px;
  flex-wrap: wrap;
}

.empty-src {
  grid-column: 1 / -1;
  border: 1px dashed var(--border-2);
  border-radius: var(--r);
  padding: 3rem;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--text-3);
  letter-spacing: 0.04em;
}

/* ── Rooftop grid ── */
.rooftop-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 0.625rem;
  margin-bottom: 2rem;
}
.rooftop-tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: 0.875rem 1rem;
  display: flex; flex-direction: column; gap: 4px;
  transition: border-color var(--ease);
}
.rooftop-tile:hover { border-color: var(--border-2); }
.rooftop-name {
  font-family: var(--font-display, var(--font-hd));
  font-size: 0.82rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text);
}
.rooftop-meta {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  color: var(--text-3);
  line-height: 1.5;
}
.rooftop-id {
  font-family: var(--font-mono);
  font-size: 0.58rem;
  color: var(--text-3);
  opacity: 0.6;
  margin-top: 4px;
}
.empty-rooftop {
  border: 1px dashed var(--border-2);
  border-radius: var(--r);
  padding: 2rem;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--text-3);
}

/* ── Account-page responsive ── */
/* Admin's .nav-right is now two ghost buttons plus the bell and the name
 * menu (api/admin/navigation.py). It once held TEN flat buttons plus the
 * email: as bordered pills they stacked THREE rows deep at 390px — a 170px
 * nav bar, a fifth of the screen, before any content, with "Sign out"
 * orphaned on its own row. The compact treatment below stays: it also covers
 * the portal bar, and keeps the admin bar to one row even if it grows.
 *
 * The size lives here rather than inline. Those anchors carried
 * style="font-size:0.72rem" in admin/templates/base.html, and an inline style
 * beats any rule in this file — which is exactly why the first attempt at this
 * did nothing. The attribute was removed in the same change. */
/* Tables must contain their own horizontal overflow.
 *
 * `.data-table--wrap` exists and was given `overflow-x: auto`, but NO template
 * uses it — verified across all 21 that render a `.data-table`. So that change
 * was a no-op and four pages still pushed the page sideways: portal inbound
 * triage (660px), portal inventory (575px), portal inventory equipment (712px)
 * and admin inbound triage (725px), all in a 390px viewport.
 *
 * `display: block` changes table sizing, so it is confined to phone widths,
 * where the alternative is a page you cannot read. Above the breakpoint it
 * stays a real table. */
@media (max-width: 680px) {
  .data-table {
    display: block;
    /* Contains absolutely positioned children (.sr-only header text): without
       it they escape this scroll box and widen the page (measured +8px on
       portal settings/team at 390px). */
    position: relative;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    max-width: 100%;
  }
}

@media (max-width: 680px) {
  .topnav .nav-right {
    gap: 0.15rem 0.45rem;
  }

  .topnav .nav-right .btn-ghost {
    border-color: transparent;
    background: transparent;
    padding: 4px 6px;
    font-size: 0.68rem;
    letter-spacing: 0.06em;
  }

}

@media (max-width: 680px) {
  .stat-bar { grid-template-columns: repeat(2, 1fr); }
  .pg-header { flex-direction: column; align-items: flex-start; }
  .pg-header-actions { flex-wrap: wrap; }
  .src-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 420px) {
  /* .btn sets white-space:nowrap, which is right on a wide screen and wrong at
     320px: a long label then has a min-content width larger than the viewport
     and pushes the whole document sideways. Measured on the file-detail page,
     where "Download bronze (15-min signed URL)" overflowed by 64px. Only
     relaxed at phone widths, so button shapes elsewhere are untouched. */
  .btn { white-space: normal; max-width: 100%; }
  .src-grid { grid-template-columns: minmax(0, 1fr); }
  .stat-bar { grid-template-columns: 1fr 1fr; }
}

/* ── Settings nav (shared tab strip; rendered by the settings_nav macro) ── */
/* Tabs WRAP rather than overflow. Three fit at 390px today; a fourth would
 * have pushed the page sideways, and a scrolling strip hides tabs behind a
 * gesture nobody performs. */
.settings-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin-bottom: 2rem;
  border-bottom: 1px solid var(--border);
  padding-bottom: 0;
}
.settings-nav a {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
  padding: 8px 14px;
  border-bottom: 2px solid transparent;
  text-decoration: none;
  transition: color var(--ease), border-color var(--ease);
}
.settings-nav a:hover { color: var(--text-2); }
.settings-nav a.active {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

/* Status pills shared by admin and portal: one definition each, so the
   page-local copies cannot drift apart again. */
.surf-chip {
  display: inline-block;
  padding: 1px 7px;
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: 0.62rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  background: var(--surface-3);
  border: 1px solid var(--border);
  color: var(--text-3);
}
.badge-email {
  background: var(--accent-dim);
  color: var(--cyan);
}
.badge-upload {
  background: var(--surface-2);
  color: var(--text-2);
}
.confidence-badge {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  padding: 2px 5px;
  border-radius: 3px;
}
.count-chip {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  color: var(--text-3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: 2px 9px;
}
.pill-list {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

/* Reconciled status pills. Each was defined in two templates and had
   drifted by exactly one property. Resolved deliberately; larger or
   tokenised value wins:
     badge-active/-disabled/-superadmin  gap: 4px  (users.html had it,
       user_detail.html did not; visible only when the badge carries an icon)
     cat-chip      font-size 0.62rem  (was 0.6rem on the recipes index)
     custom-pill   font-size 0.60rem  (was 0.58rem on the recipes index)
     pill-lbl--incompat  var(--warn, #f59e0b) — the other copy hardcoded
       #d97706, so one component rendered in two different oranges. */
.badge-active {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 7px;
  border-radius: var(--r-sm);
  font-size: 0.6rem;
  background: var(--success-bg);
  color: var(--success);
  border: 1px solid var(--success-br);
}
.badge-disabled {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 7px;
  border-radius: var(--r-sm);
  font-size: 0.6rem;
  background: var(--error-bg);
  color: var(--error);
  border: 1px solid var(--error-br);
}
.badge-superadmin {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 7px;
  border-radius: var(--r-sm);
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  background: var(--cyan-dim, var(--accent-dim));
  color: var(--cyan, var(--accent));
  border: 1px solid rgba(43,69,240,0.25);
}
.cat-chip {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 20px;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border: 1px solid var(--border-2);
  background: var(--surface-2);
  color: var(--text-3);
}
.cat-chip.acquisition {
  background: rgba(37,99,235,0.07);
  border-color: rgba(37,99,235,0.25);
  color: #2563eb;
}
.cat-chip.retention {
  background: rgba(22,163,74,0.07);
  border-color: rgba(22,163,74,0.25);
  color: #16a34a;
}
.cat-chip.reputation {
  background: rgba(124,58,237,0.07);
  border-color: rgba(124,58,237,0.25);
  color: #7c3aed;
}
.cat-chip.spend {
  background: rgba(220,38,38,0.07);
  border-color: rgba(220,38,38,0.25);
  color: #dc2626;
}
.cat-chip.content {
  background: rgba(234,88,12,0.07);
  border-color: rgba(234,88,12,0.25);
  color: #ea580c;
}
.custom-pill {
  display: inline-block;
  padding: 2px 7px;
  border-radius: 20px;
  font-family: var(--font-mono);
  font-size: 0.6rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border: 1px solid rgba(124,58,237,0.35);
  background: rgba(124,58,237,0.06);
  color: var(--accent);
}
.pill-lbl--incompat {
  border-style: dashed;
  border-color: var(--warn, #f59e0b);
  color: var(--warn, #f59e0b);
  opacity: 0.7;
}
.pill-lbl--incompat .info-i {
  color: var(--warn, #f59e0b);
  border-color: var(--warn, #f59e0b);
}

/* A data table is intrinsically wider than a phone. Without this the table
   pushed the whole document sideways at 768px. .table-wrap was already used by
   _shared/_partials/_inbound_triage_table.html and defined nowhere, so every
   page using it inherited the overflow. */
.table-wrap {
  overflow-x: auto;
  overflow-y: hidden;
}

/* A Drive folder's approval and its action (_partials/_folder_approval.html):
   the pill, then Approve or Deny, or both while pending, on one line. The
   buttons never part; the pill may sit above them, which happens only where
   the table is too narrow for one line (a pending row on a phone). A cell
   that could never wrap was measured at 390px: the pinned Actions column
   grew to cover the folder names. */
.folder-approval { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.folder-approval-actions { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.folder-approval form { display: inline; margin: 0; }
/* "Keep approved" / "Keep denied" on an inherited state: a small text link,
   quieter than the one real action beside it. */
.folder-approval-keep {
  padding: 0; border: 0; background: none; cursor: pointer;
  font: inherit; font-size: 0.78rem; color: var(--accent); text-decoration: underline;
}
.folder-approval-keep:hover { color: var(--link-hover); }
.folder-approval-keep:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Keep a table's last column (its row actions) in view while the rest scrolls.
 *
 * A table that scrolls sideways inside its own box puts the rightmost column
 * out of view whenever the table is wider than the box, and macOS hides the
 * scrollbar until you scroll. On 2026-10-09 that hid Approve/Discard on the
 * portal inbound triage page at a ~1300px window: the controls existed, nothing
 * showed they were there, and the email could not be approved.
 *
 * Pinning the cell to the right edge of the scroll box works at every width:
 * on desktop the box is .table-wrap or .data-table--wrap, and on phones
 * (max-width: 680px above) the .data-table itself is the box. The cell needs an
 * opaque background or the scrolled columns show through it; a page that sets
 * the table on a card overrides --sticky-actions-bg. Rows whose last cell spans
 * the table (empty states) are left alone. tests/test_table_actions_visible.py
 * renders the real pages and checks the controls sit inside the visible box. */
.data-table--sticky-actions > thead > tr > th:last-child,
.data-table--sticky-actions > tbody > tr > td:last-child:not([colspan]) {
  position: sticky;
  right: 0;
  z-index: 1;
  background: var(--sticky-actions-bg, var(--bg));
  box-shadow: inset 1px 0 0 var(--border);
}
/* Cards are --surface, not --bg. Pages with their own card class set the
   property on it in their template (.ab-card, .kw-card, .section-card). */
.card .data-table--sticky-actions {
  --sticky-actions-bg: var(--surface);
}
.data-table--mono.data-table--sticky-actions > thead > tr > th:last-child {
  background: var(--surface-2);
}
/* Mono cells paint --bg themselves, wherever the table sits. */
.data-table--mono.data-table--sticky-actions > tbody > tr > td:last-child:not([colspan]) {
  background: var(--bg);
}
.data-table--sticky-actions > tbody > tr:hover > td:last-child:not([colspan]) {
  background: var(--surface-2);
}
.data-table--mono.data-table--sticky-actions > tbody > tr:hover > td:last-child:not([colspan]) {
  background: var(--surface);
}
/* A selected row paints its cells --accent-dim, which is translucent, so the
   other cells show the tint over whatever is behind the table. The pinned cell
   must stay opaque, so it lays the same tint over that colour itself. This
   comes after the hover rules and outranks them, as `.data-table tr.selected td`
   outranks `.data-table tr:hover td` for the other cells. Pages that set the
   table on a card must set --sticky-actions-bg for this to match, as above. */
.data-table.data-table--sticky-actions > tbody > tr.selected > td:last-child:not([colspan]) {
  background-color: var(--sticky-actions-bg, var(--bg));
  background-image: linear-gradient(var(--accent-dim), var(--accent-dim));
}

/* ── Top-bar account menu ─────────────────────────────────────────────────
   <details>-based so it opens without JavaScript; a small script in base.html
   closes it on outside click. The trigger is the signed-in person's name, not
   a gear — the menu holds Settings AND security AND sign out, and a gear
   describes only the first (the EMR's navigation.ts records the same call). */
.nav-account { position: relative; }
.nav-account > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 12px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--text-2);
  white-space: nowrap;
  user-select: none;
}
.nav-account > summary::-webkit-details-marker { display: none; }
.nav-account > summary::after {
  content: "▾";
  font-size: 0.6rem;
  color: var(--text-3);
}
.nav-account[open] > summary,
.nav-account > summary:hover { border-color: var(--border-2); color: var(--text); }
/* Screen-reader-only text. Several portal templates already used this class
   with no rule behind it, so the text rendered visibly. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
/* Portal "Finish setup (n of 3)" pill: the Setup menu's trigger while the
   owner checklist is incomplete (api/portal/navigation.py SETUP_MENU). */
.nav-setup.is-pending > summary {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--accent-dim);
}
/* At phone width the Setup trigger sits near the LEFT of a wrapped bar, so a
   right-anchored menu would open off-screen; anchor it left there. */
@media (max-width: 680px) {
  .nav-setup .nav-account-menu { right: auto; left: 0; }
}
.nav-account-menu {
  position: absolute; right: 0; top: calc(100% + 6px);
  min-width: 190px;
  background: var(--surface);
  border: 1px solid var(--border-2);
  border-radius: var(--r);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.35);
  padding: 6px;
  z-index: 400; /* above .topnav's sticky content */
  display: flex; flex-direction: column;
}
.nav-account-menu a {
  display: block;
  padding: 7px 10px;
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--text-2);
  text-decoration: none;
  white-space: nowrap;
}
.nav-account-menu a:hover { background: var(--surface-2); color: var(--text); }
.nav-account-menu hr {
  border: none; border-top: 1px solid var(--border);
  margin: 5px 4px;
}

/* Keyboard users get the same ring buttons already have (see .btn-secondary
   above). Hover was the only visible state on the menu before this. */
.nav-account > summary:focus-visible,
.nav-bell:focus-visible,
.nav-account-menu a:focus-visible,
.pg-subnav a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The bar links' size lives HERE, not inline — an inline font-size beats the
   compact rule in the 680px block below, and a static test
   (test_admin_nav_links_have_no_inline_font_size) forbids it on the admin
   surface. Same value the portal template inlines, so no visual change. */
.topnav .nav-right > .btn-ghost { font-size: 0.72rem; }

/* ── Page sub-nav (billing) ──────────────────────────────────────────────
   The admin menu shows ONE Billing entry; /admin/billing and
   /admin/billing/rates cross-link through this strip. Sections of a page
   family, not global nav — see api/admin/navigation.py. */
.pg-subnav { display: flex; gap: 0.4rem; margin: 0 0 14px; }
.pg-subnav a {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-2);
  text-decoration: none;
  padding: 4px 10px;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
}
.pg-subnav a:hover { color: var(--text); border-color: var(--border-2); }
.pg-subnav a[aria-current="page"] {
  color: var(--text);
  border-color: var(--border-2);
  background: var(--surface-2);
}

/* ── Top-bar triage bell ──────────────────────────────────────────────────
   A queue is a count, not a place (see api/portal/navigation.py). The count
   arrives lazily by HTMX so pages without it cost nothing extra. */
.nav-bell {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--text-2);
  text-decoration: none;
  flex-shrink: 0;
}
.nav-bell:hover { border-color: var(--border-2); color: var(--text); }
.nav-bell-count {
  position: absolute; top: -6px; right: -6px;
  min-width: 15px; height: 15px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--error);
  color: #fff;
  font-family: var(--font-mono);
  font-size: 0.58rem; font-weight: 700;
  line-height: 15px; text-align: center;
}

/* ── Page section tabs ────────────────────────────────────────────────────
   Sections OF a page (the account page's Files / Inventory / Triage), sitting
   under the page header. Distinct from buttons on purpose: a tab is where you
   are, a button is something you do. Scrolls sideways on phones instead of
   wrapping, so the baseline rule survives (same fix as /portal/memory's tab
   strip, 2026-08-24). */
.pg-tabs {
  display: flex; align-items: center; gap: 2px;
  border-bottom: 1px solid var(--border);
  margin: 0.25rem 0 1.25rem;
  overflow-x: auto;
  scrollbar-width: none;
}
.pg-tabs::-webkit-scrollbar { display: none; }
.pg-tab {
  flex: 0 0 auto;
  padding: 8px 14px;
  font-family: var(--font-hd, var(--font-mono));
  font-size: 0.74rem; font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--text-3);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  white-space: nowrap;
  display: inline-flex; align-items: center; gap: 6px;
}
.pg-tab:hover { color: var(--text); }
.pg-tab.active {
  color: var(--text);
  border-bottom-color: var(--accent, var(--cyan));
}


/* ── Preview modal metadata panel ────────────────────────────────────────
   Shared markup: public/assets/files.js's mountPreviewModal() emits these
   classes into the DOM directly (not from either template), for BOTH the
   admin files dashboard and the portal files page. Round 6 found the
   original flat strip with no rules at all on the portal (files_dashboard.html
   carried its own copy, in its own <style> block, that the portal's
   template never loaded) — the third time in this project a shared JS
   component's markup has had CSS in only one surface. components.css is the
   file both base templates already <link> unconditionally, so it is the one
   true home; do not re-copy these into a per-template <style> block again.

   Replaces the old flat `.preview-meta-strip` run (seven equal-weight pairs
   in one grey inline sentence) with a panel grouped into "What it is" /
   "What we think" / "Where it's from" / Invoice / Problem — see the panel
   redesign round in docs/specs/portal-files-unified-browse.md. Group
   headings are implicit: a gap plus a rule between adjacent groups, not a
   rendered label, since the field labels themselves already carry the
   weight ("Category", "Provider", "Vendor" read as their own groups without
   a header repeating that). */
.preview-panel {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 0.75rem 1.5rem;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}
.preview-panel-group {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 10px 20px;
}
.preview-panel-group + .preview-panel-group {
  padding-top: 0.75rem;
  border-top: 1px solid var(--border);
}
.preview-field {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.preview-field-label {
  font-family: var(--font-mono);
  font-size: 0.58rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-3);
}
.preview-field-value {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--text-2);
  overflow-wrap: break-word;
}
.preview-field-value .badge { vertical-align: middle; }
.preview-field-value a {
  color: var(--cyan);
}

/* Problem group reads as a problem, not another grey pair — the same
   error tokens badge-error/conf-chip--low already use, not a new literal. */
.preview-panel-group--problem {
  background: var(--error-bg);
  border: 1px solid var(--error-br);
  border-radius: 4px;
  padding: 0.625rem 0.75rem;
  grid-template-columns: 1fr;
}
.preview-panel-group--problem .preview-field-label { color: var(--error); }
.preview-panel-group--problem .preview-field-value { color: var(--error); }


/* ── Confidence chip ─────────────────────────────────────────────────────
   Moved here from portal_files.html's own <style> block (this round) — the
   same "shared JS/markup, CSS in only one surface" bug the meta-strip block
   above already documents, found a second time in the same file. The
   preview panel's Confidence field (files.js) and the flat Library table's
   Confidence column/filter (portal_files.html) both use these classes and
   must never disagree about what a band looks like, so there is exactly
   one copy of the rule, not two that can drift. Bands come from
   sources/files/serialize.py's confidence_band(). */
.conf-chip {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 600;
  padding: 2px 7px;
  border-radius: 10px;
  white-space: nowrap;
}
.conf-chip--high     { background: var(--success-bg); color: var(--success); border: 1px solid var(--success-br); }
.conf-chip--medium   { background: var(--running-bg); color: var(--running); border: 1px solid var(--running-br); }
.conf-chip--conflict { background: var(--warning-bg); color: var(--warning); border: 1px solid var(--warning-br); }
.conf-chip--low      { background: var(--error-bg);   color: var(--error);   border: 1px solid var(--error-br); }
.conf-chip--none     { background: var(--never-bg);   color: var(--text-3);  border: 1px solid var(--never-br); }
