/* TrendAI Icons — styled to match the TrendAI™ Design System used across
   ForgeIQ and other internal TrendAI tools (see /Users/jaime_escano/Trend/IQ/
   ForgeIQ/frontend/src/styles.css for the source of truth this mirrors).
   Onest display + Work Sans body + JetBrains Mono. Brand red (#D71920)
   accent, amber (#FF9500) secondary, executive gray ramp for surfaces.
   Black canvas (dark, default) / exec-100 canvas (light, opt-in). */

@font-face {
  font-family: 'Onest';
  src: url('/fonts/Onest-VariableFont_wght.ttf') format('truetype-variations');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Work Sans';
  src: url('/fonts/WorkSans-VariableFont_wght.ttf') format('truetype-variations');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'JetBrains Mono';
  src: url('/fonts/JetBrainsMono-VariableFont_wght.ttf') format('truetype-variations');
  font-weight: 100 800;
  font-style: normal;
  font-display: swap;
}

:root {
  /* ── TrendAI brand ─────────────────────────────────────────────── */
  --trendai-red: #D71920;       /* PMS 485 C */
  --trendai-amber: #FF9500;     /* PMS 2728 C */

  /* Executive neutral ramp (from the DS) */
  --exec-900: #14171A;
  --exec-800: #1F2329;
  --exec-700: #2A2F36;
  --exec-600: #3A4049;
  --exec-500: #5A626D;
  --exec-400: #8A929C;
  --exec-300: #B5BCC4;
  --exec-200: #DCE0E5;
  --exec-100: #F0F2F5;

  --grad-headline: linear-gradient(90deg, var(--trendai-red) 40%, var(--trendai-amber) 100%);

  /* Foreground (on black) */
  --fg: #ffffff;
  --fg-muted: var(--exec-300);
  --fg-subtle: var(--exec-400);

  /* Background / surfaces */
  --bg: #000000;
  --surface: var(--exec-900);
  --surface-2: var(--exec-800);

  --border: var(--exec-700);
  --border-subtle: var(--exec-800);
  --border-strong: var(--exec-600);

  /* Fixed (not theme-swapped) — the detail panel's preview box shows
     whichever variant the user picks. The white-ink variants (Reverse-White,
     Reverse-Red-White) disappear against --surface-2's near-white tone in
     light mode, so they always get a dark backdrop; conversely "Color"'s
     black ink barely reads against that same dark backdrop in dark mode, so
     it gets a light one instead — see the .panel-preview.is-color toggle in
     app.js's renderPanelBody(). */
  --preview-bg: var(--exec-700);
  --preview-bg-light: var(--exec-100);

  --accent: var(--trendai-red);
  --accent-hover: #ee2a31;
  --accent-soft: rgba(215, 25, 32, 0.18);
  --accent-fg: #ffffff;

  --danger: #ff5d63;
  /* Approve/reinstate vs block. Named for what they mean, not what they look
     like, so the light-theme overrides can shift the hue for contrast without
     every call site having to know. Same pair the status badges use. */
  --ok: #4CAF50;
  --warn: #D9A441;
  --hover: rgba(255, 255, 255, 0.06);
  --chip-bg: rgba(255, 255, 255, 0.06);

  /* Item lifecycle status. One token per status so the edit-form tabs and the
     grid badges can't drift apart. Deliberately NOT the brand red for
     "published" — red reads as a warning next to amber/red states, and the
     accent is already spent on "this tab is selected" everywhere else. */
  --status-draft: var(--exec-400);       /* neutral — not ready, not a problem */
  --status-published: #2ea16b;           /* green — live */
  --status-deprecated: var(--trendai-amber);
  --status-retired: var(--danger);
  /* Access, not lifecycle — deliberately outside the draft→retired ramp so a
     restricted item doesn't read as "further along" or "more broken". */
  --status-restricted: #7c5cd6;

  /* Grid zoom level — set by app.js's zoom slider, scaled off these bases. */
  --tile-min: 120px;
  --tile-icon: 32px;
  --tile-font: 11px;

  /* Photos grid zoom — same mechanism, separate variables so zooming the
     icon grid and the photo grid don't affect each other. */
  --photo-tile-min: 200px;
  --photo-tile-height: 140px;
  --photo-tile-font: 12px;

  /* Logos grid zoom — same mechanism as photos, separate variables so
     zooming logos doesn't affect the icon/photo grids. */
  --logo-tile-min: 220px;
  --logo-tile-height: 120px;
  --logo-tile-font: 12px;

  /* Fonts section's preview specimen size, and the grid card min-width it
     drives (so bigger preview text gets wider, fewer-per-row cards instead
     of just wrapping more inside a fixed-width one) — both set by app.js's
     size slider. */
  --font-preview-size: 32px;
  --font-card-min: 240px;

  --radius: 14px;
  --radius-sm: 8px;
  /* Height of the header search box. Shared by .search-wrap (which is this
     tall) and .header-search (which reserves this much whether or not a
     search box is showing) so the two can't drift apart and reintroduce the
     header-resize jump they were introduced to fix. */
  --search-height: 43px;
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
  --shadow-lg: 0 18px 50px rgba(0, 0, 0, 0.55);

  --font: 'Work Sans', system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif;
  --font-display: 'Onest', 'Work Sans', system-ui, -apple-system, sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;

  /* Custom <select> chevron (native appearance is disabled in .snippet-row
     select) — baked to match this theme's --fg-muted (#B5BCC4) since a data
     URI can't reference a CSS variable. */
  --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23B5BCC4' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
}

:root[data-theme='light'] {
  --fg: var(--exec-900);
  --fg-muted: var(--exec-500);
  --fg-subtle: var(--exec-400);

  --bg: var(--exec-100);
  --surface: #ffffff;
  --surface-2: var(--exec-100);

  --border: var(--exec-200);
  --border-subtle: #E7EAEE;
  --border-strong: var(--exec-300);

  /* Matches light theme's --fg-muted (#5A626D). */
  --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235A626D' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");

  --accent-hover: #b5141a;
  --accent-soft: rgba(215, 25, 32, 0.10);
  --danger: #cf222e;
  /* Both darkened against the light theme's white surfaces — the dark-theme
     values are tuned for near-black and wash out here. */
  --ok: #2E7D32;
  --warn: #8A6100;
  --hover: rgba(0, 0, 0, 0.05);
  --chip-bg: rgba(0, 0, 0, 0.05);

  /* Darkened for contrast against the light theme's white surfaces — the
     dark-theme values are tuned for near-black and wash out here. */
  --status-draft: var(--exec-500);
  --status-published: #1a7f4f;
  --status-deprecated: #b35c00;
  --status-retired: var(--danger);
  --status-restricted: #6741c4;

  --shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
  --shadow-lg: 0 18px 50px rgba(20, 23, 26, 0.16);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  transition: background 0.2s ease, color 0.2s ease;
}

/* Outer layout: a full-width header bar on top, then a row (sidebar +
   content) filling the rest. Within .app-content, each .section's
   header/chips-or-preview-row/footer are fixed-height flex items that never
   scroll; <main> is the sole scrollable region (flex: 1 1 auto + its own
   overflow-y) — no position:sticky/fixed anywhere, which would otherwise
   leave the whole document scrolling underneath the "fixed" chrome. */
body {
  height: 100vh;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* #appShell (index.html) wraps everything below <body> so the pre-auth
   gate can hide it outright — but that makes it body's sole flex child,
   and a plain block div doesn't stretch on the main (vertical) axis of a
   column flex container by default (flex-grow:0), so it was shrinking to
   its content's own height instead of filling the viewport (visible as a
   short grid leaving a big blank gap below the status bar rather than
   pushing it to the bottom). Re-establish the same flex column context
   here that .app-header/.app-body were designed around before this wrapper
   existed. */
#appShell {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* Three-column grid (not flex) so the search box centers on the header's own
   width regardless of the brand logo's width — the two 1fr side columns
   absorb whatever space is left equally (mirrors the old per-section topbar,
   now shared by both sections instead of duplicated). */
.app-header {
  flex: none;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 24px;
  padding: 8px 24px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}
.app-brand { display: flex; align-items: center; justify-self: start; }
.app-brand img { height: 20px; width: auto; object-fit: contain; display: block; }
/* min-height, not height: the header is the tallest of brand/search/buttons,
   and the search box is the tallest of those — so without a floor here the
   whole header (and with it the sidebar and content below) jumped up 10px on
   Logos and About, which have no search box, and back down on Icons/Images.
   The header is shared chrome; it should not move when the view does. */
.header-search {
  justify-self: center;
  width: min(640px, 100%);
  min-height: var(--search-height);
}
.app-header-spacer { justify-self: end; display: flex; align-items: center; gap: 8px; }

.app-body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: row;
  overflow: hidden;
}

.sidebar {
  flex: none;
  width: 76px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 16px 0;
  background: var(--surface);
  border-right: 1px solid var(--border);
}

.sidebar-nav { display: flex; flex-direction: column; gap: 4px; width: 100%; align-items: center; }
/* Pins the reference pair — Docs, then About — to the very bottom of the
   sidebar, separate from the asset sections above them. The `auto` margin goes
   on the FIRST of the two so both travel down together; .sidebar is the flex
   column it pushes against. */
#navDocs { margin-top: auto; }
.nav-item {
  width: 60px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 10px 4px;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  color: var(--fg-muted);
  cursor: pointer;
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 600;
  transition: background 0.15s ease, color 0.15s ease;
}
/* `.nav-item` sets display:flex, which would otherwise override the [hidden]
   attribute's UA display:none — so hiding a nav tab (e.g. Fonts, when
   config.FONTS_ENABLED is off) needs this more-specific rule to actually take. */
.nav-item[hidden] { display: none; }
.nav-item:hover { background: var(--hover); color: var(--fg); }
.nav-item.active { background: var(--accent-soft); color: var(--accent); }
.nav-glyph { font-family: var(--font-display); font-weight: 700; font-size: 16px; line-height: 1; }

.app-content {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.section {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.section[hidden] { display: none; }

.about-section { overflow-y: auto; padding: 40px 32px 64px; }
.about-content { max-width: 720px; margin: 0 auto; }
.about-title {
  font-family: var(--font-display);
  font-size: 32px;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: 0 0 12px;
}
.about-lede { font-size: 15px; line-height: 1.6; color: var(--fg-muted); margin: 0 0 40px; }
.about-heading {
  font-family: var(--font-display);
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 700;
  color: var(--fg-muted);
  margin: 0 0 16px;
}
.about-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 16px;
  margin-bottom: 40px;
}
.about-card {
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius);
  padding: 20px;
}
.about-card h3 {
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: 700;
  margin: 0 0 8px;
}
.about-card p { font-size: 13px; line-height: 1.55; color: var(--fg-muted); margin: 0 0 16px; }
.about-card-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.about-contact-card {
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius);
  padding: 20px;
}
.about-contact-card p { font-size: 13px; line-height: 1.6; color: var(--fg-muted); margin: 0 0 16px; }
.about-contact-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}
.about-contact-row:last-child { margin-bottom: 0; }
.about-contact-name { font-family: var(--font-display); font-weight: 700; font-size: 14px; }
.about-contact-links { display: flex; gap: 8px; }

/* ── Docs page ─────────────────────────────────────────────────────────────
   Owns its own scrollbar like .about-section, because the table of contents
   has to stay put while the article moves — `position: sticky` needs to be
   inside the scrolling box, not the window. */
/* Deliberately NO `scroll-behavior: smooth` here. It isn't only a hint about
   animation — it makes every programmatic scroll on this container smooth,
   including the plain scrollIntoView the contents links use, and smooth
   scrolling is a silent no-op in some engines (measured: zero pixels moved).
   The result was a table of contents whose links did nothing. An instant jump is
   what a real anchor does anyway. */
.docs-section { overflow-y: auto; padding: 40px 32px 96px; }
/* Full width on purpose: these pages carry wide comparison tables (statuses,
   item types, who-sees-what), and the previous centred 1060px column made every
   one of them wrap into an unreadable stack while two thirds of a 4K screen sat
   empty. The nav column is fixed; the article takes everything else. */
.docs-layout {
  display: grid;
  grid-template-columns: 264px minmax(0, 1fr);
  gap: 40px;
  width: 100%;
  align-items: start;
}

.docs-nav {
  position: sticky;
  top: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-top: 2px;
}
.docs-search-wrap { padding-right: 4px; }
.docs-search {
  width: 100%;
  font: inherit;
  font-size: 13px;
  color: var(--fg);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 8px 11px;
}
.docs-search::placeholder { color: var(--fg-subtle); }
.docs-search:focus { outline: none; border-color: var(--accent); }
/* The native clear affordance in WebKit sits on a dark field as a white blob. */
.docs-search::-webkit-search-cancel-button { filter: invert(0.6); }

.docs-nav-tree { display: flex; flex-direction: column; gap: 1px; }
.docs-nav-empty { font-size: 12.5px; color: var(--fg-subtle); padding: 4px 10px; margin: 0; }

.docs-nav-guide, .docs-nav-section, .docs-nav-result {
  font: inherit;
  text-align: left;
  background: none;
  border: none;
  cursor: pointer;
  border-radius: 6px;
  transition: color 0.12s ease, background 0.12s ease;
}
.docs-nav-guide {
  font-size: 13px;
  font-weight: 600;
  color: var(--fg-muted);
  padding: 8px 10px;
  /* Rail on the open branch, matching the section rail below it so the two
     levels read as one hierarchy rather than two lists. */
  border-left: 2px solid transparent;
}
.docs-nav-guide:hover { color: var(--fg); background: var(--hover); }
.docs-nav-branch.open > .docs-nav-guide {
  color: var(--accent);
  border-left-color: var(--accent);
  background: var(--accent-soft);
}
.docs-nav-num { color: var(--fg-subtle); font-variant-numeric: tabular-nums; }
.docs-nav-branch.open > .docs-nav-guide .docs-nav-num { color: inherit; opacity: 0.75; }

/* Sections only exist for the open guide — a tree with every branch expanded is
   just the flat list this replaced. */
.docs-nav-subs { display: none; flex-direction: column; gap: 1px; padding: 2px 0 8px; }
.docs-nav-branch.open > .docs-nav-subs { display: flex; }
.docs-nav-section {
  font-size: 12.5px;
  line-height: 1.35;
  color: var(--fg-subtle);
  padding: 5px 10px 5px 20px;
  border-left: 2px solid transparent;
  display: flex;
  gap: 6px;
}
.docs-nav-section:hover { color: var(--fg); background: var(--hover); }
.docs-nav-section.active {
  color: var(--fg);
  font-weight: 600;
  border-left-color: var(--border-strong);
  background: var(--hover);
}

/* Search results replace the tree: one flat list, guide name above the section
   so a hit is identifiable without opening it. */
.docs-nav-tree.is-results { gap: 2px; }
.docs-nav-result {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 7px 10px;
  color: var(--fg-muted);
}
.docs-nav-result:hover { background: var(--hover); }
.docs-nav-result-guide {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--fg-subtle);
}
.docs-nav-result-title { font-size: 12.5px; color: var(--fg); }

/* Overview cards — the other way into a guide, for a reader who doesn't yet know
   which one they need. */
.docs-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 12px;
  margin-top: 8px;
}
.docs-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  text-align: left;
  font: inherit;
  cursor: pointer;
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius);
  padding: 16px 18px;
  transition: border-color 0.12s ease, background 0.12s ease;
}
.docs-card:hover { border-color: var(--border-strong); background: var(--surface); }
.docs-card-title {
  font-family: var(--font-display);
  font-size: 14.5px;
  font-weight: 700;
  color: var(--fg);
}
.docs-card-blurb { font-size: 12.5px; line-height: 1.55; color: var(--fg-muted); }
.docs-card[data-docs-tier] .docs-card-title::after {
  content: "";
  display: inline-block;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  margin-left: 7px;
  vertical-align: 2px;
  background: var(--accent);
}
.docs-card[data-docs-tier="admin"] .docs-card-title::after { background: var(--status-restricted); }

/* Guide pages: one on screen at a time (app.js's showDocsGuide). */
.docs-guide[hidden] { display: none; }
.docs-guide-head { margin-bottom: 36px; }
.docs-h1 {
  font-family: var(--font-display);
  font-size: 30px;
  font-weight: 700;
  letter-spacing: -0.015em;
  margin: 0 0 10px;
  display: flex;
  align-items: baseline;
  gap: 12px;
  flex-wrap: wrap;
}
.docs-guide-blurb {
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--fg-muted);
  margin: 0;
  max-width: 70ch;
}

.docs-tier-note {
  max-width: 70ch;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--fg-subtle);
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius);
  padding: 10px 14px;
  margin: -20px 0 40px;
}

.docs-article { min-width: 0; }
.docs-block { margin-bottom: 52px; scroll-margin-top: 8px; }
.docs-block:last-child { margin-bottom: 0; }
.docs-h2 {
  font-family: var(--font-display);
  font-size: 21px;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: 0 0 14px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border-subtle);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}
.docs-h3 {
  font-family: var(--font-display);
  font-size: 14.5px;
  font-weight: 700;
  margin: 28px 0 8px;
  display: flex;
  align-items: center;
  gap: 8px;
}
/* The PAGE is full width; the prose isn't. A 200-character line is measurably
   harder to read than a 90-character one, so paragraphs, lists and definition
   lists cap their measure while tables, cards and code blocks use the whole
   width — which is what the width was wanted for. */
.docs-article p, .docs-list, .docs-dl, .docs-note, .docs-legend { max-width: 78ch; }
.docs-article p { font-size: 13.5px; line-height: 1.65; color: var(--fg-muted); margin: 0 0 12px; }
.docs-article a { color: var(--accent); }
.docs-article code {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 12px;
  background: var(--chip-bg);
  border-radius: 4px;
  padding: 1px 5px;
}
.docs-article b { color: var(--fg); font-weight: 600; }

/* One line under a section heading saying when this is the right tool — the
   sections that were promoted out of "Three ways to use it" each need that
   framing, which the old numbered sub-headings carried inline. */
.docs-lede-small {
  font-size: 13px;
  color: var(--fg-subtle);
  margin: -6px 0 14px !important;
}
.docs-list { font-size: 13.5px; line-height: 1.65; color: var(--fg-muted); margin: 0 0 12px; padding-left: 22px; }
.docs-list li { margin-bottom: 7px; }
.docs-list-steps li { margin-bottom: 10px; }

/* Marks which audience a section is for. Sits in the h2, right-aligned, so the
   boundary between "everyone" and "your extra powers" is obvious while
   scrolling. */
.docs-tier-chip {
  font-family: var(--font-display);
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent);
  background: var(--accent-soft);
  border-radius: 20px;
  padding: 3px 10px;
  white-space: nowrap;
}
.docs-tier-chip-admin {
  color: #fff;
  background: var(--status-restricted);
}

.docs-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12.5px;
  margin: 0 0 16px;
  /* Long prose in a cell wraps rather than forcing a horizontal scroll — these
     tables carry explanations, not numbers. */
  table-layout: auto;
}
.docs-table th, .docs-table td {
  text-align: left;
  vertical-align: top;
  padding: 9px 12px;
  border-bottom: 1px solid var(--border-subtle);
  line-height: 1.5;
  color: var(--fg-muted);
}
.docs-table th {
  font-family: var(--font-display);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--fg-subtle);
  border-bottom-color: var(--border);
}
.docs-table td:first-child { color: var(--fg); white-space: nowrap; }
.docs-table-compact td, .docs-table-compact th { padding: 6px 12px; }

.docs-dl { margin: 0 0 12px; font-size: 13.5px; line-height: 1.6; }
.docs-dl dt {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--fg-subtle);
  margin-top: 10px;
}
.docs-dl dd { margin: 3px 0 0; color: var(--fg-muted); }
.docs-dl-wide dt { font-size: 13px; text-transform: none; letter-spacing: 0; color: var(--fg); }

/* One card per status, so the four don't blur into a wall of prose — each is a
   self-contained answer to "what happens if I pick this". */
.docs-status-card {
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius);
  padding: 14px 18px 16px;
  margin-bottom: 12px;
}
.docs-status-card .docs-h3 { margin-top: 0; }

.docs-note {
  font-size: 13px;
  line-height: 1.6;
  color: var(--fg-muted);
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  /* Accent rail rather than a tinted fill: these are asides worth noticing, not
     warnings, and a page with a dozen coloured boxes stops flagging anything. */
  border-left: 3px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 11px 14px;
  margin: 0 0 14px;
}
/* The handful that genuinely bite: the restriction override, the
   restricted-by-default upload, delete being permanent. */
.docs-note-strong {
  border-left-color: var(--trendai-amber);
  background: var(--surface);
}

/* Legend rows pairing a real badge/glyph with what it means. */
.docs-legend { list-style: none; margin: 0; padding: 0; }
.docs-legend li {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 11px 0;
  border-bottom: 1px solid var(--border-subtle);
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--fg-muted);
}
.docs-legend li:last-child { border-bottom: none; }
.docs-legend-mark { flex: none; width: 92px; padding-top: 1px; }
.docs-legend-lock { display: flex; justify-content: center; }

/* The status pills are absolutely positioned on a tile; inline in prose they
   need to sit in the text flow instead. Both class names in the selector on
   purpose: .hidden-badge's `position: absolute` is declared further down this
   file, so at equal specificity it wins and every badge on this page escapes to
   the top-left corner of the window — which is exactly what happened. */
.hidden-badge.docs-inline-badge {
  position: static;
  display: inline-block;
  vertical-align: 1px;
}
/* Same purple disc as the lock on a tile, without the artwork to sit on. */
.docs-legend-lock .docs-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--status-restricted);
  color: #fff;
}
.docs-glyph { display: inline-flex; align-items: center; color: var(--fg-subtle); }
.docs-glyph svg { display: block; }
.docs-h3 .docs-glyph { color: var(--fg); }

/* Below this the two columns won't both fit, so the navigation moves above the
   article rather than squeezing it into a gutter. */
@media (max-width: 900px) {
  .docs-section { padding: 28px 20px 64px; }
  .docs-layout { grid-template-columns: minmax(0, 1fr); gap: 20px; }
  .docs-nav {
    position: static;
    padding-bottom: 14px;
    border-bottom: 1px solid var(--border-subtle);
  }
  /* Guides become a wrapped row of chips; the open guide's sections stay in a
     column beneath, since those are the ones you're navigating. */
  .docs-nav-tree:not(.is-results) { flex-direction: row; flex-wrap: wrap; gap: 4px; }
  .docs-nav-branch { display: contents; }
  .docs-nav-guide { border-left: none; border-bottom: 2px solid transparent; }
  .docs-nav-branch.open > .docs-nav-guide { border-left-color: transparent; border-bottom-color: var(--accent); }
  .docs-nav-branch.open > .docs-nav-subs { flex-basis: 100%; }
  .docs-tier-note { margin-top: 0; }
  .docs-h1 { font-size: 24px; }
  .docs-legend li { flex-direction: column; gap: 6px; }
  .docs-legend-mark { width: auto; }
}

::selection { background: var(--trendai-red); color: #fff; }

/* Theme-aware scrollbars — main (the icon grid), .panel (the detail
   slide-in), .preview-editor (the fonts preview box, once it grows past its
   max-height), .mcp-modal (the MCP/Releases modals, once their content
   overflows max-height), and .mcp-code-block pre (the Claude Desktop config
   snippet, which scrolls horizontally on long lines) are the only scrolling
   containers; without this the native OS/browser scrollbar renders light
   gray regardless of theme (and, on .mcp-modal specifically, as a square-
   edged native scrollbar butting up against the modal's rounded corners). */
main, .panel, .preview-editor, .mcp-modal, .mcp-code-block pre,
.about-section, .docs-section {
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}
main::-webkit-scrollbar, .panel::-webkit-scrollbar, .preview-editor::-webkit-scrollbar,
.mcp-modal::-webkit-scrollbar,
.about-section::-webkit-scrollbar, .docs-section::-webkit-scrollbar { width: 10px; }
/* height (not width) governs a horizontal-only scrollbar's thickness —
   thinner here since it sits inside a small code block, not a full pane. */
.mcp-code-block pre::-webkit-scrollbar { height: 8px; }
main::-webkit-scrollbar-track, .panel::-webkit-scrollbar-track, .preview-editor::-webkit-scrollbar-track,
.mcp-modal::-webkit-scrollbar-track, .mcp-code-block pre::-webkit-scrollbar-track,
.about-section::-webkit-scrollbar-track, .docs-section::-webkit-scrollbar-track { background: transparent; }
main::-webkit-scrollbar-thumb, .panel::-webkit-scrollbar-thumb, .preview-editor::-webkit-scrollbar-thumb,
.mcp-modal::-webkit-scrollbar-thumb, .mcp-code-block pre::-webkit-scrollbar-thumb,
.about-section::-webkit-scrollbar-thumb, .docs-section::-webkit-scrollbar-thumb {
  background: var(--border-strong);
  border-radius: 999px;
  border: 2px solid transparent;
  background-clip: padding-box;
}
main::-webkit-scrollbar-thumb:hover, .panel::-webkit-scrollbar-thumb:hover, .preview-editor::-webkit-scrollbar-thumb:hover,
.mcp-modal::-webkit-scrollbar-thumb:hover, .mcp-code-block pre::-webkit-scrollbar-thumb:hover,
.about-section::-webkit-scrollbar-thumb:hover, .docs-section::-webkit-scrollbar-thumb:hover {
  background: var(--fg-subtle);
  background-clip: padding-box;
}

.theme-toggle {
  flex: none;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--fg-muted);
  border-radius: 999px;
  width: 32px;
  height: 32px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  font-size: 15px;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.theme-toggle:hover { background: var(--hover); border-color: var(--border-strong); color: var(--fg); }

.search-wrap {
  width: 100%;
  min-height: var(--search-height);
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 8px 16px;
}
.search-wrap[hidden] { display: none; }
.search-icon { color: var(--fg-subtle); flex: none; }
/* Toggled by setSearchBusy() (app.js) while a search/filter fetch is in
   flight — these can take several seconds against a cold search backend
   (semantic index warm-up), and without this the box gave no indication
   anything was happening between typing and results appearing. Swaps the
   static magnifier for a spinning ring rather than adding a second icon
   next to it, so the search box's footprint never changes width. */
.search-wrap.is-loading .search-icon { display: none; }
.search-wrap .search-spinner {
  display: none;
  flex: none;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 2px solid var(--border-strong);
  border-top-color: var(--fg-subtle);
  animation: search-spin 0.7s linear infinite;
}
.search-wrap.is-loading .search-spinner { display: block; }
@keyframes search-spin {
  100% { transform: rotate(360deg); }
}
.search-wrap input {
  flex: 1;
  background: transparent;
  border: none;
  outline: none;
  color: var(--fg);
  font: inherit;
  font-size: 15px;
}
.search-wrap input::placeholder { color: var(--fg-subtle); }

/* Segmented pill group on a shared gray track (TrendAI DS — mirrors
   ForgeIQ's PillGroup.vue: one background, borderless segments, the active
   one filled solid). */

/* Icons/Images/Logos filter row: the editor's status filter and the category
   chips share one line rather than stacking into two pill rows that push the
   grid down. Wraps only when there genuinely isn't room. */
.filter-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 12px;
  margin: 16px 24px;
  flex: none;
}
/* The chips carry their own margin and sizing when they're the only thing on
   the row (non-editors never see the status filter); inside the bar it's the
   bar's job. `flex: 1 1 0` is load-bearing: a wrapping flex container lines
   items up by their *hypothetical* size before any shrinking, so with the
   default `auto` basis the whole chips group (far wider than the leftover
   space) would drop to its own line below the status filter. A zero basis lets
   it take exactly what the status filter and New button leave, and wrap its
   chips inside that. */
.filter-bar .chips { margin: 0; max-width: none; min-width: 0; flex: 1 1 0; }

.chips {
  display: flex;
  /* Wraps rather than scrolling. This used to be `nowrap` + `overflow-x: auto`
     with the scrollbar hidden, which silently clipped the row: on Images,
     "Sports & Recreation" and "Healthcare" were simply invisible, with nothing
     on screen to suggest the row continued. A hidden scrollbar is only a good
     trade when the clipped content is optional; here each chip is a filter with
     a count, and a filter you can't see is a filter you don't have. */
  flex-wrap: wrap;
  gap: 2px;
  width: fit-content;
  max-width: calc(100% - 48px);
  margin: 16px 24px;
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  /* 18px, not 999px: at one line (~34px tall) these render identically as a
     stadium, but a wrapped two-line group under 999px turns into a giant
     lozenge. This keeps the familiar shape when it fits and degrades to a plain
     rounded box when it doesn't. */
  border-radius: 18px;
  padding: 4px;
  flex: none;
  row-gap: 4px;
}
.chip {
  font-family: var(--font-display);
  border: none;
  background: none;
  color: var(--fg-muted);
  border-radius: 999px;
  padding: 6px 14px;
  font-size: 13px;
  font-weight: 600;
  line-height: 1.3;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
  white-space: nowrap;
}
.chip:hover:not(.active) { color: var(--fg); }
.chip.active { background: var(--accent); color: var(--accent-fg); box-shadow: var(--shadow); }

/* Fonts section's custom-preview-text row — sits where .chips sits for
   Icons, same row/margin rhythm, but it's a small rich-text editor (type or
   paste multi-line text, select a range and Bold/Italic/Underline just that
   part) rather than a pill group. The B/I/U column sits in front; the
   editor stretches (flex default align-items:stretch) to match its height. */
.preview-row { margin: 16px 24px; flex: none; display: flex; gap: 8px; }
.preview-editor {
  flex: 1;
  min-width: 0;
  overflow-y: auto;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 16px;
  color: var(--fg);
  font: inherit;
  font-size: 14px;
  line-height: 1.4;
  outline: none;
}
.preview-editor:focus { border-color: var(--border-strong); }
/* contenteditable has no native placeholder attribute — a lone stray <br>
   left behind after clearing all text still counts as a child, so app.js
   normalizes back to a truly empty element on input for this to keep
   matching. */
.preview-editor:empty::before { content: attr(data-placeholder); color: var(--fg-subtle); }

.style-toggles { display: flex; flex-direction: column; gap: 4px; flex: none; }
.style-toggle {
  width: 40px;
  height: 40px;
  flex: none;
  padding: 0;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--fg-muted);
  font-family: var(--font-display);
  font-size: 15px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.style-toggle:hover { background: var(--hover); border-color: var(--border-strong); color: var(--fg); }
.style-toggle.active { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); }

/* Fonts browsing area — two view modes toggled via the Grid/List tabs in
   the footer (#fontList swaps its own layout + which child markup app.js
   builds; see renderFontRow()/renderFontCard()). */

/* List mode: Google-Fonts-style rows — family name/meta on one line, a
   large live @font-face specimen below, a divider between families. */
.font-list.is-list { display: flex; flex-direction: column; }
.font-row {
  padding: 20px 0;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
}
.font-row:first-child { padding-top: 0; }
.font-row-meta { display: flex; align-items: baseline; gap: 10px; margin-bottom: 12px; }
.font-row-name { font-family: var(--font-display); font-size: 15px; font-weight: 700; }
.font-row-detail { font-size: 12px; color: var(--fg-muted); }
.font-row-specimen {
  color: var(--fg);
  font-size: var(--font-preview-size);
  line-height: 1.3;
  overflow-wrap: anywhere;
}
.font-row:hover .font-row-name { color: var(--accent); }

/* Grid mode (default): compact cards, same tile-grid pattern as the icon
   grid — name/meta up top, a clipped specimen filling the rest.
   auto-fit (not auto-fill, unlike the icon grid) collapses empty tracks so
   the few real cards stretch to fill the row instead of leaving trailing
   blank space — the icon grid always has enough tiles to fill every track,
   but with only a handful of font families that's often not true. */
.font-list.is-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(var(--font-card-min), 1fr));
  gap: 12px;
}
.font-card {
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-height: 150px;
  padding: 16px;
  border-radius: var(--radius);
  background: var(--surface-2);
  border: 1px solid transparent;
  cursor: pointer;
  transition: background 0.12s, border-color 0.12s;
}
.font-card:hover { background: var(--hover); border-color: var(--border); }
.font-card-meta { display: flex; flex-direction: column; gap: 2px; }
.font-card-name { font-family: var(--font-display); font-size: 14px; font-weight: 700; }
.font-card-detail { font-size: 11px; color: var(--fg-muted); }
.font-card-specimen {
  flex: 1;
  color: var(--fg);
  font-size: var(--font-preview-size);
  line-height: 1.3;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.font-card:hover .font-card-name { color: var(--accent); }

main {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 0 24px 24px;
}

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--tile-min), 1fr));
  gap: 4px;
}

/* Uppercase category group header in the "browse all" grouped view — mirrors
   ForgeIQ's ColumnsBrowser.vue .grp-label.sub treatment. */
.section-header {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  margin: 28px 0 6px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  font-family: var(--font-display);
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--fg-subtle);
}
.section-header:first-child { margin-top: 0; padding-top: 0; border-top: none; }

.tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* flex-start, not center — a CSS Grid row is as tall as its tallest cell
     (e.g. a neighboring tile whose name wraps to 2 lines), and centering
     each tile's own content independently within that shared row height
     made icons/names drift up or down relative to tiles in the same row
     that didn't wrap. Top-aligning keeps every tile's icon+name starting at
     the exact same offset regardless of a neighbor's row-height-inflating
     wrapped name; any extra space lands at the bottom instead. */
  justify-content: flex-start;
  gap: 10px;
  padding: 18px 8px;
  border-radius: var(--radius);
  cursor: pointer;
  background: var(--surface-2);
  border: 1px solid transparent;
  transition: background 0.12s, border-color 0.12s;
  min-height: 108px;
}
.tile:hover { background: var(--hover); border-color: var(--border); }
/* width:100% (not var(--tile-icon)) — a fixed square box starved wide/short
   logos (brand lockups etc.) of the horizontal room the tile actually has,
   rendering them tiny even though the tile itself was plenty wide. A square
   icon still ends up looking identical: object-fit:contain centers it at
   --tile-icon x --tile-icon within the wider box, just with empty space on
   either side instead of a tight-fitting container. */
.tile img { width: 100%; height: var(--tile-icon); object-fit: contain; transition: height 0.1s; }
.tile .tile-name {
  font-size: var(--tile-font);
  color: var(--fg-muted);
  text-align: center;
  line-height: 1.3;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

/* Photo grid — edge-to-edge cover thumbnails (real photographs, not icon
   glyphs on a flat background), so this is deliberately a different look
   from .tile: no padding around the image, object-fit:cover fills a fixed-
   height box instead of contain-fitting within one, caption below. */
.photo-grid { grid-template-columns: repeat(auto-fill, minmax(var(--photo-tile-min), 1fr)); gap: 12px; }
.photo-tile {
  display: flex;
  flex-direction: column;
  cursor: pointer;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface-2);
  border: 1px solid transparent;
  transition: border-color 0.12s;
}
.photo-tile:hover { border-color: var(--border-strong); }
.photo-tile img {
  width: 100%;
  height: var(--photo-tile-height);
  object-fit: cover;
  display: block;
  background: var(--surface);
  transition: height 0.1s;
}
.photo-tile .photo-tile-name {
  font-size: var(--photo-tile-font);
  color: var(--fg-muted);
  padding: 8px 10px;
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.load-more-wrap { display: flex; flex-direction: column; align-items: center; }
/* Infinite-scroll sentinel: an invisible strip the IntersectionObserver
   watches (see app.js setupInfiniteScroll). When it scrolls into view the
   next page loads automatically — no button to click. Given a little height
   so it's a reliable intersection target rather than a zero-size element. */
.scroll-sentinel { width: 100%; height: 1px; }
.empty { color: var(--fg-muted); padding: 40px 0; text-align: center; width: 100%; }

.scrim {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 10;
}

.panel {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(420px, 100vw);
  background: var(--surface);
  border-left: 1px solid var(--border);
  z-index: 11;
  overflow-y: auto;
  /* No top padding — the sticky header below supplies it, so it can pin flush
     to the panel's top edge rather than leaving a gap content scrolls through. */
  padding: 0 24px 40px;
  box-shadow: var(--shadow-lg);
}

.panel-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  /* Stays put while the body scrolls, so the item you're looking at is always
     named and Close (plus Edit/Save/Cancel on an editable panel) is always one
     click away — these panels run to several screens on a logo with dozens of
     files. Bled out to the panel's own padding so nothing scrolls through the
     gap either side of it. */
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--surface);
  margin: 0 -24px 16px;
  padding: 20px 24px 12px;
  border-bottom: 1px solid var(--border);
}
.panel-header h2 {
  font-family: var(--font-display);
  font-size: 20px;
  margin: 0;
  font-weight: 700;
  letter-spacing: -0.01em;
  /* Long names wrap rather than pushing the buttons off the panel edge. */
  min-width: 0;
  overflow-wrap: anywhere;
}
.icon-btn {
  background: transparent;
  border: none;
  color: var(--fg-muted);
  font-size: 16px;
  cursor: pointer;
  padding: 4px 8px;
  border-radius: 6px;
  flex: none;
}
.icon-btn:hover { background: var(--hover); color: var(--fg); }

.mcp-connect-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 13px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--fg-muted);
  border-radius: 999px;
  padding: 7px 14px;
  /* Below ~1100px "Connect to Claude" wrapped to two lines, which grew the
     header by 7px — but only in the views that also show a search box to
     compete for the room, so the header changed height as you moved between
     them. Keeping the label on one line is what stops that. */
  white-space: nowrap;
  flex: none;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.mcp-connect-btn:hover { background: var(--hover); border-color: var(--border-strong); color: var(--fg); }

/* A centered floating card (not the slide-in .panel) — reuses .scrim for
   the backdrop, since only one of the two ever needs to be open at once. */
.mcp-modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(560px, calc(100vw - 32px));
  max-height: calc(100vh - 64px);
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: 20px 24px 24px;
  z-index: 11;
}
/* Settings only, not every .mcp-modal: this one carries two tables (external
   contacts, and five address lists) whose columns had nowhere to go at the
   shared 560px — the contact rows wrapped their action buttons and the pager
   broke across lines. "Connect to Claude" is a paragraph and a code block and
   stays narrow. */
.releases-modal { width: min(860px, calc(100vw - 32px)); }

.mcp-modal-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
}
.mcp-modal-header h2 {
  font-family: var(--font-display);
  font-size: 18px;
  margin: 0;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.mcp-modal-desc { font-size: 13px; color: var(--fg-muted); line-height: 1.5; margin: 0 0 16px; }

.mcp-code-block {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius);
  padding: 16px;
}
.mcp-code-block pre {
  flex: 1;
  min-width: 0;
  margin: 0;
  overflow-x: auto;
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.5;
  color: var(--fg);
  white-space: pre;
}
.mcp-code-block .copy-btn { flex: none; }

.watermark-section {
  padding-bottom: 16px;
  margin-bottom: 16px;
  border-bottom: 1px solid var(--border);
}
.watermark-section h3,
.releases-heading {
  font-family: var(--font-display);
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 700;
  color: var(--fg-muted);
  margin: 0 0 8px;
}
.watermark-status-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 4px;
}
.watermark-status-text { font-size: 13px; color: var(--fg-muted); }
.watermark-status-text.is-suppressed { color: var(--trendai-amber); }
.watermark-actions { display: flex; gap: 8px; flex: none; }

.release-type + .release-type { margin-top: 20px; }
.release-type-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
}
.release-type-header h3 {
  font-family: var(--font-display);
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 700;
  color: var(--fg-muted);
  margin: 0;
}
.releases-global-actions { display: flex; justify-content: flex-end; margin-top: 12px; }

/* Indeterminate — the stages (scanning / deriving N of M / rebuilding a
   font) are too heterogeneous in duration for a fake unified percentage to
   be honest, so this shows "it's moving" via an animated sliding fill,
   paired with real stage text rather than a precise number. */
.release-progress { margin-top: 8px; }
.release-progress-track {
  position: relative;
  height: 4px;
  border-radius: 999px;
  background: var(--surface-2);
  overflow: hidden;
}
.release-progress-fill {
  position: absolute;
  top: 0;
  left: -40%;
  width: 40%;
  height: 100%;
  border-radius: 999px;
  background: var(--accent);
  animation: release-progress-slide 1.1s ease-in-out infinite;
}
@keyframes release-progress-slide {
  0% { left: -40%; }
  100% { left: 100%; }
}
.release-progress-text { margin-top: 6px; font-size: 12px; color: var(--fg-muted); }
.release-versions {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.release-version-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  padding: 8px 12px;
}
.release-version-row.is-active { border-color: var(--accent); }
.release-version-info { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.release-version-tag {
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 700;
  color: var(--fg);
  flex: none;
}
.release-version-meta {
  font-size: 12px;
  color: var(--fg-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.release-active-badge {
  flex: none;
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 700;
  color: var(--accent);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.release-status-msg { font-size: 12px; color: var(--fg-muted); margin-top: 8px; }

.mcp-howto { margin-top: 16px; font-size: 13px; color: var(--fg-muted); }
.mcp-howto summary { cursor: pointer; color: var(--fg); font-weight: 600; margin-bottom: 8px; }
.mcp-howto ol { margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 6px; }
.mcp-howto code {
  font-family: var(--mono);
  font-size: 11px;
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: 4px;
  padding: 2px 5px;
}

.panel-preview-wrap { position: relative; margin-bottom: 20px; }
.panel-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--preview-bg);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius);
  padding: 32px;
  overflow: hidden;
}
.panel-preview.is-color { background: var(--preview-bg-light); }
/* width:100% (not a fixed 96px square) — same reasoning as .tile img: a
   square box left wide/short logos (brand lockups etc.) tiny and barely
   legible even though the preview box has plenty of spare width. Height
   bumped up too since the box itself has much more room here than a grid
   tile does. Scaled by --panel-zoom (see panelZoomIn/Out in app.js) so
   someone can zoom in to inspect fine detail — .panel-preview's own
   overflow:hidden clips it back to the rounded box at high zoom instead of
   spilling past the corners. */
.panel-preview img {
  width: 100%;
  height: 220px;
  object-fit: contain;
  transform: scale(var(--panel-zoom, 1));
  transition: transform 0.1s;
}
.panel-preview-zoom {
  position: absolute;
  right: 12px;
  bottom: 12px;
  display: flex;
  align-items: center;
  gap: 6px;
  background: var(--surface);
  border: 1px solid var(--border-subtle);
  border-radius: 999px;
  padding: 4px 8px;
}

.panel-section { margin-bottom: 32px; }
.panel-label {
  font-family: var(--font-display);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-weight: 600;
  color: var(--fg-muted);
  margin-bottom: 12px;
}

/* Segmented pill group, same shared-track treatment as the category chips
   (TrendAI DS PillGroup.vue). */
.tabs {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 2px;
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: 999px;
  padding: 3px;
}
.tab {
  font-family: var(--font-display);
  font-weight: 600;
  border: none;
  background: none;
  color: var(--fg-muted);
  border-radius: 999px;
  padding: 6px 12px;
  font-size: 13px;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.tab:hover:not(.active) { color: var(--fg); }
.tab.active { background: var(--accent); color: var(--accent-fg); box-shadow: var(--shadow); }

/* Status tabs colour-code by meaning rather than all sharing the accent, so
   the consequence of the choice is legible before you read the label. The
   inactive options carry a dot in the same colour — otherwise the coding is
   only visible once selected, which is exactly when you no longer need it. */
.tab[data-status].active { background: var(--status-color); color: #fff; }
.tab[data-status] { display: inline-flex; align-items: center; gap: 6px; }
.tab[data-status]::before {
  content: "";
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--status-color);
  flex: none;
}
.tab[data-status].active::before { background: #fff; }
.tab[data-status="draft"] { --status-color: var(--status-draft); }
.tab[data-status="published"] { --status-color: var(--status-published); }
.tab[data-status="deprecated"] { --status-color: var(--status-deprecated); }
.tab[data-status="retired"] { --status-color: var(--status-retired); }

/* Photo size pills — all five (Thumbnail/Small/Medium/Large/Original) must
   fit on one row in the 420px panel, so this variant is more compact than
   the default .tab and never wraps. */
#photoSizeTabs { display: flex; flex-wrap: nowrap; width: 100%; }
#photoSizeTabs .tab { flex: 1 1 auto; padding: 6px 8px; font-size: 12px; white-space: nowrap; text-align: center; }

.downloads { display: flex; gap: 8px; flex-wrap: wrap; }
.download-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-display);
  font-weight: 600;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--fg);
  border-radius: 999px;
  padding: 8px 16px;
  font-size: 13px;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}
button.download-btn { appearance: none; }
button.download-btn:disabled { opacity: 0.5; cursor: wait; }
.download-btn:hover { background: var(--hover); border-color: var(--border-strong); transform: translateY(-1px); }

/* One row per weight/style in the font detail panel — a live "Aa" specimen
   in that exact weight, plus its own OTF/WOFF2 download buttons. */
.font-style-list { display: flex; flex-direction: column; gap: 12px; }
.font-style-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 12px;
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
}
.font-style-glyph { font-size: 28px; color: var(--fg); flex: none; width: 40px; text-align: center; }
.font-style-info { flex: 1; min-width: 0; }
.font-style-name { font-family: var(--font-display); font-weight: 600; font-size: 13px; }
.font-style-weight { font-size: 11px; color: var(--fg-muted); }
.font-style-downloads { display: flex; gap: 6px; flex: none; }
.font-style-downloads .download-btn { padding: 5px 12px; font-size: 12px; }

.snippet-row { margin-bottom: 14px; }
.snippet-row:last-child { margin-bottom: 0; }
.snippet-title { display: block; font-size: 12px; color: var(--fg-muted); margin-bottom: 6px; }

.snippet-row select {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  max-width: 220px;
  background: var(--surface-2) var(--select-arrow) no-repeat right 14px center;
  background-size: 14px;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius);
  padding: 11px 38px 11px 16px;
  font: inherit;
  font-size: 13px;
  color: var(--fg);
  cursor: pointer;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.snippet-row select:hover { border-color: var(--border-strong); }
.mcp-token-expires-at { margin-left: 10px; font-size: 12px; color: var(--fg-muted); }
.snippet-row select:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.snippet-box {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
}
.snippet-box code {
  flex: 1;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--fg);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.copy-btn {
  flex: none;
  font-family: var(--font-display);
  font-weight: 600;
  background: transparent;
  border: 1px solid var(--border);
  color: var(--fg-muted);
  border-radius: 6px;
  padding: 4px 10px;
  font-size: 11px;
  cursor: pointer;
}
.copy-btn:hover { background: var(--hover); color: var(--fg); }
.copy-btn.copied { color: var(--accent); border-color: var(--accent); }

/* "New" sits in the filter bar with the curation controls, and is the only
   affirmative action there — hence the accent border rather than the muted
   outline the copy/ghost buttons use. `margin-left: auto` pushes it to the right
   edge, ahead of the status filter. */
.new-item-btn {
  flex: none;
  margin-left: auto;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 11px;
  padding: 5px 12px;
  border-radius: 999px;
  cursor: pointer;
  background: transparent;
  color: var(--accent);
  border: 1px solid var(--accent);
}
.new-item-btn:hover { background: var(--accent); color: #fff; }

/* The naming step. A row rather than a modal: it's one field, and the grid behind
   it is useful context for "is this already here under another name". */
.new-item-row {
  margin: 0 24px 12px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}
.new-item-label {
  display: block;
  font-family: var(--font-display);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 600;
  color: var(--fg-muted);
  margin-bottom: 8px;
}
.new-item-controls { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.new-item-controls input {
  flex: 1 1 260px;
  min-width: 0;
  font: inherit;
  font-size: 13px;
  padding: 7px 10px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--fg);
}
.new-item-controls input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.webfont-preview {
  display: flex;
  align-items: center;
  gap: 14px;
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  padding: 14px;
  margin-bottom: 10px;
}
.webfont-preview .ti { font-size: 32px; color: var(--fg); flex: none; }
.webfont-hint { font-size: 11px; color: var(--fg-muted); line-height: 1.4; }

.panel-categories { display: flex; flex-wrap: wrap; gap: 6px; }
.panel-categories span {
  font-size: 11px;
  font-weight: 600;
  color: var(--accent);
  background: var(--accent-soft);
  border-radius: 999px;
  padding: 3px 10px;
}

.photo-dimensions { font-size: 12px; color: var(--fg-muted); margin-top: 8px; }
.photo-description { font-size: 13px; line-height: 1.5; color: var(--fg-muted); }

/* Icon grid zoom control — a full-width status bar fixed to the very bottom
   of the viewport (document-viewer convention), zoom controls right-aligned.
   Only affects --tile-min/--tile-icon/--tile-font (the grid), not page
   chrome. No "fit width"/"fit page" equivalents — there's no analogous
   concept for a tile grid, just the slider + in/out/reset. */
.status-bar {
  flex: none;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 6px 16px;
  background: var(--surface);
  border-top: 1px solid var(--border);
}
.status-text {
  font-family: var(--font-display);
  font-size: 12px;
  font-weight: 600;
  color: var(--fg-muted);
}
.zoom-controls { display: flex; align-items: center; gap: 8px; margin-left: auto; }

/* Buttons here need an explicit reset — there's no project-wide `button {}`
   base style, so without one these fall back to the browser's native (light,
   OS-themed) button chrome regardless of our dark canvas. */
.status-btn {
  flex: none;
  width: 22px;
  height: 22px;
  padding: 0;
  border: none;
  background: none;
  color: var(--fg-muted);
  border-radius: 6px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font: inherit;
  -webkit-appearance: none;
  appearance: none;
}
.status-btn:hover { background: var(--hover); color: var(--fg); }
.status-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.zoom-reset {
  min-width: 38px;
  text-align: center;
  cursor: pointer;
  font: inherit;
  -webkit-appearance: none;
  appearance: none;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 3px 10px;
  color: var(--fg-muted);
}
.zoom-reset:hover { background: var(--hover); color: var(--fg); border-color: var(--border-strong); }
.zoom-reset:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Fully custom track/thumb — `accent-color` alone still leaves the native
   (light, blue-focus-ring) track/outline showing through in some browsers. */
.zoom-slider {
  width: 120px;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
}
.zoom-slider:focus { outline: none; }
.zoom-slider::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 999px;
  background: var(--border-strong);
}
.zoom-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 14px;
  height: 14px;
  margin-top: -5px;
  border-radius: 50%;
  background: var(--accent);
  border: none;
  cursor: pointer;
}
.zoom-slider:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.zoom-slider::-moz-range-track {
  height: 4px;
  border-radius: 999px;
  background: var(--border-strong);
}
.zoom-slider::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--accent);
  border: none;
  cursor: pointer;
}
.zoom-slider:focus-visible::-moz-range-thumb {
  box-shadow: 0 0 0 3px var(--accent-soft);
}

@media (max-width: 600px) {
  .zoom-slider { width: 70px; }
}

/* ── Metadata editor (photo panel) + Access panel (Settings) + hidden state ── */
.meta-edit { display: flex; flex-direction: column; gap: 10px; }
/* display:flex above would otherwise beat the [hidden] attribute — so the
   collapsed edit form (toggled by the "Edit" button) actually stays hidden. */
.meta-edit[hidden] { display: none; }
.meta-field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--fg-muted); }
.meta-field input[type="text"],
.meta-field textarea {
  width: 100%;
  box-sizing: border-box;
  padding: 8px 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--fg);
  font: inherit;
  font-size: 13px;
}
.meta-field textarea { resize: vertical; }
.meta-field input:focus,
.meta-field textarea:focus { outline: none; border-color: var(--accent); }
.meta-field textarea[readonly] { opacity: 0.7; cursor: default; }
.tag-input {
  display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
  padding: 6px 8px; min-height: 38px;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius-sm); cursor: text;
}
.tag-input:focus-within { border-color: var(--accent); }
.tag-input input {
  flex: 1; min-width: 80px; border: none; outline: none;
  background: transparent; color: var(--fg); font: inherit; font-size: 13px;
  padding: 2px 0;
}
.tag-chip {
  display: inline-flex; align-items: center; gap: 4px;
  background: var(--surface-3, #e8e8e8); color: var(--fg); border-radius: var(--radius-sm);
  padding: 4px 10px; font-size: 12px; font-weight: 500;
  white-space: nowrap; border: 1px solid var(--border);
}
.tag-chip button {
  appearance: none; border: none; background: none; color: inherit;
  cursor: pointer; font-size: 14px; line-height: 1; padding: 0 2px;
  opacity: 0.7;
}
.tag-chip button:hover { opacity: 1; }
.meta-edit-actions { display: flex; align-items: center; gap: 10px; }
.meta-edit-status { font-size: 12px; color: var(--fg-muted); }

/* ── Item editor (logo panel) ───────────────────────────────────────────── */
/* The read-only panel is sized for a preview and a few snippets; the editor
   adds a file table with an access dropdown and two buttons per row, which
   doesn't fit in 420px. Widen only while the form is open, so browsing keeps
   the narrower panel. */
.panel.is-editing { width: min(760px, 100vw); }

/* The panel header carries the Edit / Save / Cancel buttons alongside Close.
   Wraps so a long item name doesn't force them off the edge on a narrow
   panel; align-items keeps them on the title's first line when they fit. */
.panel-header-actions {
  display: flex; align-items: center; justify-content: flex-end;
  flex-wrap: wrap; gap: 8px; flex-shrink: 0;
}
.panel-header-actions [hidden] { display: none; }
/* The save/upload status message shares the header row, so cap it rather than
   letting "Failed: …" squeeze the buttons. */
.panel-header-actions .meta-edit-status { max-width: 220px; }

/* A quieter button than .copy-btn, for the many low-stakes row-level actions
   in the file table where a full-weight button per row would shout. */
.ghost-btn {
  appearance: none; cursor: pointer;
  padding: 3px 10px;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--fg-muted);
  font: inherit; font-size: 12px;
  white-space: nowrap;
}
.ghost-btn:hover { background: var(--hover); color: var(--fg); }
.ghost-btn:disabled { opacity: 0.5; cursor: default; }
.ghost-btn.danger:hover { border-color: var(--danger); color: var(--danger); }

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

.meta-check {
  display: flex; align-items: center; gap: 8px;
  margin-top: 10px; font-size: 12px; color: var(--fg-muted); cursor: pointer;
}

/* A switch rather than a checkbox for Restricted, because it reads as a state
   being turned on ("this item IS restricted") rather than an option being ticked
   in a list — and because it now heads its own section, where a bare checkbox
   looked like a stray item from Status above. Purple when on, matching the lock
   mark and the padlock filter. The real <input> stays in the DOM, visually
   hidden but focusable, so keyboard and screen-reader behaviour are the
   browser's own. */
.switch-row {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 12px;
  color: var(--fg-muted);
  cursor: pointer;
}
.switch-input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}
.switch-track {
  flex: none;
  position: relative;
  width: 34px;
  height: 20px;
  border-radius: 999px;
  background: var(--surface-3, rgba(127, 127, 127, 0.35));
  border: 1px solid var(--border);
  transition: background 0.15s, border-color 0.15s;
}
.switch-knob {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
  transition: transform 0.15s;
}
.switch-input:checked + .switch-track {
  background: var(--status-restricted);
  border-color: var(--status-restricted);
}
.switch-input:checked + .switch-track .switch-knob { transform: translateX(14px); }
/* Focus has to land on the track, since the input itself is invisible. */
.switch-input:focus-visible + .switch-track {
  outline: 2px solid var(--status-restricted);
  outline-offset: 2px;
}
.switch-label { line-height: 1.4; }

/* Sub-heading inside a panel section — the guest list under the Restricted
   switch is part of that section, not a section of its own. */
.panel-sublabel {
  margin: 14px 0 8px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 600;
  color: var(--fg-subtle);
}

.editor-hint { margin: 8px 0 0; font-size: 11px; color: var(--fg-subtle); }

/* Danger zone. Separated by a rule and set apart in red because it's the only
   irreversible control in the app — everything else in this form can be edited
   back. Deliberately NOT the brand accent red used for Save: an action that
   destroys data shouldn't wear the same colour as the one that keeps it. */
.danger-zone {
  border-top: 1px solid var(--border);
  margin-top: 24px;
  padding-top: 18px;
}
.danger-label { color: #d0524f; }
.danger-actions { display: flex; align-items: center; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.danger-btn {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 11px;
  padding: 5px 12px;
  border-radius: 6px;
  cursor: pointer;
  background: transparent;
  color: #d0524f;
  border: 1px solid color-mix(in srgb, #d0524f 55%, transparent);
}
.danger-btn:hover:not(:disabled) { background: color-mix(in srgb, #d0524f 12%, transparent); }
.danger-btn:disabled { opacity: 0.45; cursor: not-allowed; }
/* The armed state is filled, so the second click is visibly not the first. */
.danger-btn-armed {
  background: #d0524f;
  border-color: #d0524f;
  color: #fff;
}
.danger-btn-armed:hover:not(:disabled) { background: #b8433f; }
/* The access-scope line answers "what did that actually do?", so it carries a
   little more weight than the static hint below it without becoming a warning —
   nothing here is wrong, it's just easy to misread. */
.editor-hint-scope { color: var(--fg); }

/* The file table is the source table plus two control columns. The source
   table sizes its name column with a max-width:0 trick, which only behaves
   when the other columns are content-sized — the controls here aren't, so
   this one is laid out fixed with the two control columns pinned and the
   filename taking whatever's left. */
.file-table { table-layout: fixed; width: 100%; }
.file-table th:nth-child(2), .file-table td:nth-child(2) { width: 108px; }
.file-table th:nth-child(3), .file-table td:nth-child(3) { width: 164px; }
/* Indent the first column to the same inset as the group band's label, so the
   filenames read as sitting under their access group rather than starting to
   the left of it. Qualified with .file-table to outrank `.source-table td`'s
   padding, which is defined later in this file. */
.file-table th:first-child, .file-table td:first-child { padding-left: 10px; }
.file-table .source-name { max-width: none; width: auto; }
.file-table .file-actions { display: flex; gap: 6px; justify-content: flex-end; }
.file-access-select, .upload-axes select {
  padding: 3px 6px;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius-sm); color: var(--fg);
  font: inherit; font-size: 12px;
}
.file-access-select { width: 100%; }
.file-access-select:disabled { opacity: 0.5; }

/* Access group headers within the file table. A tinted band so the split
   reads at a glance rather than as just another row of small caps.
   The <td> must stay a real table cell: giving it display:flex drops it out
   of the table layout, which makes colspan a no-op and knocks every column
   below it out of alignment — so the flex lives on an inner wrapper. Both
   rules are qualified with .source-table to outrank the generic
   `.source-table td` padding/border further down this file. */
.source-table .file-group-cell {
  padding: 14px 10px 7px;
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
}
.file-table tr:first-child .file-group-cell { padding-top: 7px; }
.file-group {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--fg-muted);
}
.file-group-warn, .format-warn { color: var(--status-deprecated); text-transform: none; letter-spacing: 0; }
.format-warn { margin-left: 5px; font-weight: 700; cursor: help; }

.dropzone {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  flex-wrap: wrap;
  padding: 20px 12px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--fg-muted); font-size: 12px; text-align: center;
}
.dropzone.is-over { border-color: var(--accent); background: var(--hover); }
.dropzone.has-file { border-style: solid; border-color: var(--accent); }
/* The chosen filename comes from the JS via data-filename rather than a
   dedicated element, so the zone has one layout in both states. */
.dropzone.has-file::after {
  content: attr(data-filename);
  flex-basis: 100%;
  color: var(--fg);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
}

.upload-axes {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 10px; margin-top: 12px;
}
.upload-preview { margin-top: 10px; font-size: 12px; color: var(--fg-muted); }
.upload-preview code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px; color: var(--fg);
}
.access-grid { display: flex; flex-direction: column; gap: 12px; margin: 8px 0 12px; }

/* Editor filter controls, parked on the right of the icons/images/logos
   filter bar. Icon-only on purpose: this is a small-audience curation control
   sharing a line with the category chips, which are the thing most people
   came for, so it buys its space back for them. Every button carries a
   `title` and an `aria-label` with the full name and count.

   Two separate controls: `status` is a lifecycle (an item is exactly one of
   draft/published/deprecated/retired) while `restricted` is an access flag
   that cuts across all four — a published item can also be restricted.
   Folding restricted in as a sixth status pill made those look mutually
   exclusive, and made "restricted drafts" impossible to ask for. */
.status-filter {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: none;
}
/* display:flex above would otherwise beat the [hidden] attribute (same fix as
   .nav-item/.section/.meta-edit/.bulk-controls — see those comments) — without
   this the editor-only filter stays visible for every signed-in user
   regardless of access.isEditor. */
.status-filter[hidden] { display: none; }

/* The four lifecycle options plus "all" read as one segmented control, since
   picking one always deselects the others. */
.status-pills {
  display: flex;
  align-items: center;
  gap: 2px;
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: 999px;
  padding: 2px;
}

.status-icon-btn {
  position: relative;
  width: 26px;
  height: 26px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--fg-muted);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.status-icon-btn:hover:not(.active) { color: var(--fg); background: var(--surface-1); }
.status-icon-btn.active { background: var(--status-color, var(--accent)); color: #fff; }
.status-icon-btn svg { flex: none; }
.status-icon-btn[data-status="draft"] { --status-color: var(--status-draft); }
.status-icon-btn[data-status="published"] { --status-color: var(--status-published); }
.status-icon-btn[data-status="deprecated"] { --status-color: var(--status-deprecated); }
.status-icon-btn[data-status="retired"] { --status-color: var(--status-retired); }

/* Only the exception states carry a visible count, and only when it's
   non-zero — a badge on Draft is news worth acting on, whereas All and
   Published just restate the item total the status bar already shows. */
.status-icon-btn .status-count {
  position: absolute;
  top: -3px;
  right: -3px;
  min-width: 15px;
  height: 15px;
  padding: 0 4px;
  box-sizing: border-box;
  border-radius: 999px;
  background: var(--status-color, var(--fg-muted));
  border: 1.5px solid var(--surface-2);
  color: #fff;
  font-family: var(--font-display);
  font-size: 9px;
  font-weight: 700;
  line-height: 12px;
  text-align: center;
}

/* Deliberately outside the segmented group: restricted combines with a
   status rather than replacing it, and the gap plus its own border is what
   says so. */
.restricted-toggle {
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  padding: 0;
  color: var(--fg-muted);
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: 999px;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.restricted-toggle:hover:not(.active) { color: var(--fg); }
.restricted-toggle.active {
  background: var(--status-restricted);
  border-color: var(--status-restricted);
  color: #fff;
  box-shadow: var(--shadow);
}
.restricted-toggle svg { flex: none; }
.restricted-toggle .status-count {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 15px;
  height: 15px;
  padding: 0 4px;
  box-sizing: border-box;
  border-radius: 999px;
  background: var(--status-restricted);
  border: 1.5px solid var(--bg);
  color: #fff;
  font-family: var(--font-display);
  font-size: 9px;
  font-weight: 700;
  line-height: 12px;
  text-align: center;
}

/* Tooltips for the icon-only filter buttons. Built in CSS rather than with a
   native `title` because browsers hold that back for roughly a second, and
   these labels aren't a nicety — they're the only thing naming what each icon
   filters to. app.js sets `data-tip` and a matching aria-label, and does NOT
   set `title`, which would otherwise pop a second tooltip a beat later. */
.status-icon-btn[data-tip]::after,
.restricted-toggle[data-tip]::after {
  content: attr(data-tip);
  position: absolute;
  top: calc(100% + 9px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 40;
  width: max-content;
  max-width: 190px;
  padding: 6px 9px;
  border-radius: var(--radius-sm);
  background: var(--fg);
  color: var(--bg);
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 600;
  line-height: 1.35;
  text-align: center;
  white-space: normal;
  box-shadow: var(--shadow);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;  /* never let the tooltip eat the hover that spawned it */
}
.status-icon-btn[data-tip]::before,
.restricted-toggle[data-tip]::before {
  content: "";
  position: absolute;
  top: calc(100% + 4px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 41;
  border: 5px solid transparent;
  border-bottom-color: var(--fg);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
/* Shown outright, with no delay and no fade. "As soon as the mouse is over
   it" is the whole requirement, and a transition here would also be the one
   thing that can't run in a backgrounded tab, where the compositor is
   paused — the tooltip would stay invisible at opacity 0. */
.status-icon-btn[data-tip]:hover::after,
.status-icon-btn[data-tip]:focus-visible::after,
.restricted-toggle[data-tip]:hover::after,
.restricted-toggle[data-tip]:focus-visible::after,
.status-icon-btn[data-tip]:hover::before,
.status-icon-btn[data-tip]:focus-visible::before,
.restricted-toggle[data-tip]:hover::before,
.restricted-toggle[data-tip]:focus-visible::before {
  opacity: 1;
  visibility: visible;
}
/* The filter sits hard against the right margin, so the last two buttons'
   tooltips are right-aligned instead of centred or they'd run off-screen. */
.status-pills > .status-icon-btn:last-child[data-tip]::after,
.restricted-toggle[data-tip]::after {
  left: auto;
  right: 0;
  transform: none;
}
.status-pills > .status-icon-btn:last-child[data-tip]::before,
.restricted-toggle[data-tip]::before {
  left: auto;
  right: 8px;
  transform: none;
}

/* Editor status views: dim non-published tiles + corner badge */
.photo-tile, .tile { position: relative; }
.photo-tile.is-hidden-asset img,
.tile.is-hidden-asset img { opacity: 0.45; }

/* Stand-in for a tile with no public thumbnail (restricted items never get
   one — see routes_items.py's _thumbnail_url), in place of a broken <img>. */
.tile-locked {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  aspect-ratio: 1;
  color: var(--fg-muted);
  background: var(--surface-2);
  border-radius: var(--radius-sm);
}
/* The em dash carries the whole message here, so it needs to be legible rather
   than the 1rem body size a glyph-less box inherits. */
.tile-empty {
  font-size: 0.85rem;
  line-height: 1.2;
}
/* The logo row's own glyph — its label beneath already says "No files yet". */
.logo-variant-preview .tile-empty {
  font-size: 1.6rem;
  line-height: 1;
}
.hidden-badge {
  position: absolute;
  top: 6px;
  left: 6px;
  /* Colour-coded to match the status tabs in the editor (--status-* tokens),
     so "Deprecated" reads the same on a grid tile as in the form. */
  background: var(--status-color, rgba(0, 0, 0, 0.72));
  color: #fff;
  font-size: 10px;
  font-weight: 600;
  padding: 2px 6px;
  border-radius: 4px;
  letter-spacing: 0.02em;
}
.hidden-badge[data-status="draft"] { --status-color: var(--status-draft); }
.hidden-badge[data-status="deprecated"] { --status-color: var(--status-deprecated); }
.hidden-badge[data-status="retired"] { --status-color: var(--status-retired); }
/* Restricted is a separate dimension from status, so it gets a different SHAPE,
   not just a different colour in the same pill: a round purple lock badge on the
   opposite corner of the artwork. Two identical pills side by side read as two
   values of one field, and at grid scale a glyph is recognised where a word has
   to be read. The white ring keeps it legible over both light artwork (logos on
   white) and dark (Inverted/White treatments on near-black). */
.restricted-mark {
  position: absolute;
  top: 6px;
  right: 6px;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--status-restricted);
  color: #fff;
  box-shadow: 0 0 0 1.5px rgba(255, 255, 255, 0.9), 0 1px 3px rgba(0, 0, 0, 0.35);
  pointer-events: auto;
}
.restricted-mark svg { display: block; }
/* Inline in a panel label, where it names the state rather than marking artwork:
   no absolute positioning, no ring, and small enough to sit on the text
   baseline. */
.panel-label .restricted-mark {
  position: static;
  width: 18px;
  height: 18px;
  box-shadow: none;
  vertical-align: -4px;
  margin-right: 6px;
}
/* The restricted callout in the detail panel: tinted so "you need permission for
   this" registers before the download buttons below it do. */
.restricted-callout {
  border: 1px solid color-mix(in srgb, var(--status-restricted) 40%, transparent);
  background: color-mix(in srgb, var(--status-restricted) 8%, transparent);
  border-radius: var(--radius);
  padding: 12px 14px;
}
.restricted-who-line { margin: 6px 0 0; font-size: 12px; word-break: break-word; }
/* Disclosure header for the callout. The default triangle is replaced with a
   caret that rotates on open, so the control reads the same as the rest of the
   panel's typography instead of like a raw browser widget. */
.restricted-summary {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  list-style: none;
  font-family: var(--font-display);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 600;
  color: var(--fg-muted);
}
.restricted-summary::-webkit-details-marker { display: none; }
.restricted-summary::after {
  content: "";
  margin-left: auto;
  width: 6px;
  height: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg);
  transition: transform 0.15s;
}
details[open] > .restricted-summary::after { transform: rotate(-135deg); }
.restricted-summary:focus-visible {
  outline: 2px solid var(--status-restricted);
  outline-offset: 3px;
  border-radius: 4px;
}
/* The lock in the summary sits in a flex row, so it needs no baseline nudge —
   unlike the .panel-label case above. */
.restricted-summary .restricted-mark {
  position: static;
  width: 20px;
  height: 20px;
  box-shadow: none;
  vertical-align: baseline;
  margin: 0;
}
/* The logo grid dims a whole row for a non-published status; the lock must stay
   at full strength, since "you need permission" is not a lifecycle state. */
.is-hidden-asset .restricted-mark { opacity: 1; }

/* ── Logos & Brand Assets ──────────────────────────────────────────────── */
/* One row per logo, its treatments laid out across the row (wrapping) — the
   logo plays the part a category chip does elsewhere, so every variant is
   visible at once instead of one panel-click deep. */
.logo-rows { display: flex; flex-direction: column; gap: 28px; }
.logo-row { display: flex; flex-direction: column; gap: 10px; position: relative; }
.logo-row-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  position: relative;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--border-subtle);
}
.logo-row-title {
  font-family: var(--font-display);
  border: none;
  background: none;
  padding: 0;
  color: var(--fg);
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}
.logo-row-title:hover { color: var(--accent); }
.logo-row-count { color: var(--fg-subtle); font-size: 12px; }
.logo-row-items {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--logo-tile-min), 1fr));
  gap: 16px;
}
.logo-variant-tile {
  border: 1px solid var(--border-subtle);
  border-radius: 10px;
  overflow: hidden;
  cursor: pointer;
  background: var(--surface);
  transition: border-color 0.15s ease;
}
.logo-variant-tile:hover { border-color: var(--border-strong); }
.logo-variant-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--logo-tile-height);
  padding: 16px;
  box-sizing: border-box;
  background: var(--surface-2);
  /* Positioning context for .restricted-mark, which sits over the artwork. */
  position: relative;
}
/* Swatch behind each transparent-background logo: light-on-transparent marks
   (White/Inverted) get a dark one, dark-on-transparent marks (Black/
   Full-Color) get a light one — fixed colors, not theme vars, since it's the
   artwork that decides, not the page theme (see applyLogoSwatch). */
.logo-variant-preview.on-dark { background: #1a1d21; }
.logo-variant-preview.on-light { background: #ffffff; }
.logo-variant-preview img { max-width: 100%; max-height: 100%; object-fit: contain; }
/* Empty-state copy inside a preview frame. The frame carries a light or
   checkerboard swatch, so panel text colour (light-on-dark) disappears into it. */
.logo-preview-empty {
  font-size: 12px;
  color: #4a4f57;
  text-align: center;
  padding: 0 16px;
}
.logo-variant-name {
  padding: 8px 10px;
  font-size: var(--logo-tile-font);
  color: var(--fg-muted);
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Non-published logo: badge in the row's top-right (the default top-left
   would sit on top of the row title) and every variant preview dimmed. */
.logo-row > .hidden-badge { left: auto; right: 0; top: 0; }
.logo-row.is-hidden-asset .logo-variant-preview img { opacity: 0.45; }
.logo-treatment-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 160px;
  padding: 20px;
  box-sizing: border-box;
  /* Sits directly above the VARIANT section, which supplies no top margin of
     its own — without this the label butts straight against the preview box. */
  margin-bottom: 20px;
  background:
    repeating-conic-gradient(var(--surface-2) 0 25%, transparent 0 50%) 0 0 / 20px 20px,
    var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.logo-treatment-preview.on-dark {
  background: #1a1d21;
  background-image: repeating-conic-gradient(#26292e 0 25%, #1a1d21 0 50%);
  background-size: 20px 20px;
}
.logo-treatment-preview.on-light {
  background: #ffffff;
  background-image: repeating-conic-gradient(#eeeeee 0 25%, #ffffff 0 50%);
  background-size: 20px 20px;
}
.logo-treatment-preview img { max-width: 100%; max-height: 200px; }

/* Source-file listing (icon/logo/image panels) — a sorted table rather than
   a row of buttons, since a single logo can carry nearly thirty masters and
   they're only distinguishable by name, format and size. */
.source-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
}
.source-table th {
  text-align: left;
  font-weight: 600;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--fg-subtle);
  padding: 0 8px 6px 0;
  border-bottom: 1px solid var(--border);
}
.source-table td {
  padding: 6px 8px 6px 0;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}
.source-table tr:last-child td { border-bottom: none; }
.source-name {
  color: var(--fg);
  /* Long master filenames shouldn't push the size/download columns out of
     the panel — clip instead, with the full name on the row's tooltip. */
  max-width: 0;
  width: 100%;
  overflow: hidden;
}
.source-label,
.source-filename {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.source-filename {
  color: var(--fg-subtle);
  font-size: 11px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  margin-top: 1px;
}
.source-format {
  display: inline-block;
  padding: 1px 6px;
  border: 1px solid var(--border);
  border-radius: 4px;
  font-size: 10px;
  font-weight: 600;
  color: var(--fg-muted);
}
.source-size { color: var(--fg-muted); white-space: nowrap; text-align: right; }
.source-action { text-align: right; padding-right: 0; }
.source-action .download-btn { padding: 2px 10px; font-size: 12px; }

/* Bulk "Download all ▾" control + its format/size popover (filter bar).
   Note the explicit [hidden] override: .dl-menu sets display:flex, which would
   otherwise beat the [hidden] attribute's UA display:none (same trap as
   .meta-edit / .nav-item elsewhere in this file). */
.dl-wrap { position: relative; flex: none; margin-left: auto; }
.dl-btn {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 11px;
  padding: 5px 12px;
  border-radius: 999px;
  cursor: pointer;
  background: transparent;
  color: var(--fg-muted);
  border: 1px solid var(--border);
  white-space: nowrap;
}
.dl-btn:hover { color: var(--fg); border-color: var(--border-strong); }
.dl-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  min-width: 150px;
  padding: 6px;
  gap: 2px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
}
.dl-menu[hidden] { display: none; }
.dl-menu button {
  text-align: left;
  font-size: 12px;
  padding: 7px 10px;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--fg);
  cursor: pointer;
}
.dl-menu button:hover { background: var(--hover); }

/* ── External access table (Settings modal) ─────────────────────────────────
   Replaced a free-text textarea of emails once external contacts gained
   records (status, company, who approved them) and a self-service request
   flow. A table is the only honest rendering of that: the old control could
   express "these addresses are allowed" and nothing else, and saving it wrote
   the whole list at once — one stray keystroke revoked everybody. */
.ext-toolbar {
  display: flex;
  gap: 8px;
  align-items: center;
  margin: 14px 0 10px;
  flex-wrap: wrap;
}
.ext-search {
  flex: 1;
  min-width: 180px;
  font: inherit;
  font-size: 12px;
  color: var(--fg);
  background: var(--field-bg, transparent);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 5px 10px;
}
.ext-search:focus, .ext-select:focus { outline: none; border-color: var(--accent); }
.ext-select {
  font: inherit;
  font-size: 12px;
  color: var(--fg);
  background: var(--field-bg, transparent);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 5px 8px;
}
/* display:inline-block below would otherwise beat the [hidden] attribute's UA
   display:none — same trap as .nav-item/.meta-edit/.dl-menu above — and the
   badge would sit there reading "0 pending" forever, which is precisely the
   state it exists to NOT announce. */
.ext-pending-badge[hidden] { display: none; }
.ext-pending-badge {
  display: inline-block;
  margin-left: 8px;
  font-size: 11px;
  font-weight: 700;
  padding: 2px 9px;
  border-radius: 999px;
  background: var(--accent);
  color: #fff;
  vertical-align: middle;
}

.ext-add-form {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 14px;
  margin-bottom: 12px;
}
.ext-add-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 10px;
}
.ext-add-grid input {
  font: inherit;
  font-size: 12px;
  color: var(--fg);
  background: var(--field-bg, transparent);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 5px 8px;
  width: 100%;
}
.ext-add-grid input:focus { outline: none; border-color: var(--accent); }

/* The modal is a fixed width but the row actions aren't compressible below a
   few buttons' worth, so the table scrolls sideways rather than wrapping
   every cell into an unreadable column. */
.ext-table-wrap { overflow-x: auto; }
.ext-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
}
.ext-table th {
  text-align: left;
  font-weight: 600;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--fg-subtle);
  padding: 0 10px 6px 0;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.ext-table td {
  padding: 8px 10px 8px 0;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}
.ext-table tr:last-child td { border-bottom: none; }
.ext-contact-name { color: var(--fg); font-weight: 600; }
.ext-contact-email { color: var(--fg-muted); }
.ext-contact-title { color: var(--fg-subtle); font-size: 11px; }
.ext-actions-col { text-align: right; }
.ext-row-actions {
  display: flex;
  gap: 6px;
  justify-content: flex-end;
  flex-wrap: wrap;
}
.ext-when { color: var(--fg-muted); white-space: nowrap; }
.ext-table td { max-width: 220px; }
.ext-contact-email, .ext-contact-title { overflow-wrap: anywhere; }

/* Status is the column people scan for, so it carries colour; the palette
   matches the confirm page (web/app/access-request.html) so the same record
   doesn't look like two different things in two places. */
.ext-badge {
  display: inline-block;
  font-size: 11px;
  font-weight: 700;
  padding: 2px 9px;
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--fg-muted);
  white-space: nowrap;
}
.ext-badge.pending { border-color: #B8860B; color: #D9A441; }
.ext-badge.active { border-color: #2E7D32; color: #4CAF50; }
.ext-badge.blocked { border-color: var(--danger, #D71920); color: var(--danger, #D71920); }

/* The row actions differ in consequence, not just in label: approving grants
   access, blocking refuses someone permanently, deleting forgets them. Colour
   carries that so the destructive one isn't one careless click away from the
   routine one. Outlined rather than filled — three filled buttons per row, on
   every row, would read as an alert. */
.copy-btn.action-ok { border-color: var(--ok); color: var(--ok); }
.copy-btn.action-ok:hover { background: var(--ok); color: #fff; }
.copy-btn.action-warn { border-color: var(--warn); color: var(--warn); }
.copy-btn.action-warn:hover { background: var(--warn); color: #fff; }
.copy-btn.action-danger { border-color: var(--danger); color: var(--danger); }
.copy-btn.action-danger:hover { background: var(--danger); color: #fff; }

/* One quiet line, and only when there's more than one page to move between —
   "Page 1 of 1" next to two dead buttons is three controls saying nothing.
   Text links rather than bordered buttons for the same reason: paging is
   navigation, and it shouldn't compete with the row actions above it. */
.ext-pager {
  display: flex;
  gap: 14px;
  align-items: baseline;
  justify-content: flex-end;
  margin-top: 10px;
  font-size: 12px;
  white-space: nowrap;
}
.ext-pager button {
  flex: none;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 12px;
  background: none;
  border: none;
  padding: 0;
  color: var(--fg-muted);
  cursor: pointer;
}
.ext-pager button:hover:not(:disabled) { color: var(--accent); }
.ext-pager button:disabled { opacity: 0.35; cursor: default; }
.ext-page-label { color: var(--fg-subtle); }
/* Status sits left, paging controls right. */
#extStatus { margin-right: auto; white-space: normal; }

/* ── Email-list editors (Settings) ──────────────────────────────────────────
   The five address lists (seed admins, additional admins, marketing editors,
   restricted-item access, access-request approvers). Each was a textarea of
   newline-separated emails; see app.js's createEmailListEditor for why that
   had to change. Deliberately plainer than the external-contacts table above
   — these hold a handful of colleagues, not a growing roster, so they get a
   scroll cap instead of paging. */
.email-list-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.email-list-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--fg-subtle);
}
.email-list-wrap {
  border: 1px solid var(--border);
  border-radius: 8px;
  /* Roughly five rows before it scrolls: enough that a normal list is fully
     visible, capped so one long list can't push the Add box off the panel. */
  max-height: 170px;
  overflow-y: auto;
}
.email-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
}
.email-table td {
  padding: 6px 10px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}
.email-table tr:last-child td { border-bottom: none; }
.email-cell {
  color: var(--fg);
  overflow-wrap: anywhere;
}
.email-actions {
  text-align: right;
  white-space: nowrap;
  width: 1%;
}
.email-empty {
  margin: 0;
  padding: 10px;
  font-size: 12px;
  color: var(--fg-subtle);
}
.email-add {
  display: flex;
  gap: 8px;
}
.email-add input {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-size: 12px;
  color: var(--fg);
  background: var(--field-bg, transparent);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 5px 10px;
}
.email-add input:focus { outline: none; border-color: var(--accent); }
.email-add .copy-btn { flex: none; }
/* A rejected address is the one message here that must not be missed — it's
   the difference between "added" and "silently not added". */
.email-list-field .meta-edit-status.error { color: var(--danger, #D71920); }
