/* =============================================================================
   THE WIRE — STYLESHEET
   -----------------------------------------------------------------------------
   Tailwind CSS v4, loaded CSS-first through the @tailwindcss/vite plugin. There
   is deliberately no tailwind.config.js: design tokens live in the `@theme`
   block below and dark mode is driven by a `.dark` class on <html>.

   Contents
     1. Tailwind import + dark-mode variant registration
     2. `@theme` design tokens (newspaper palette, fonts, motion, shadows)
     3. Semantic CSS variables that flip between light and dark
     4. Components: surfaces, rules, buttons, fields, panels, badges
     5. Components: dialogs, toasts, switches, nav underline
     6. Components: the Newsroom Panel shell
     7. Newspaper detail: halftone, drop cap, washi tape
     8. Utilities, keyframes, reduced-motion + print fallbacks
   ========================================================================== */

/* 1. TAILWIND + DARK MODE ---------------------------------------------------- */
@import 'tailwindcss';

/* Tailwind v4 defaults to `prefers-color-scheme`; we override it so the theme
   controller in src/lib/theme.js owns the decision via <html class="dark">. */
@custom-variant dark (&:where(.dark, .dark *));

/* 2. DESIGN TOKENS ----------------------------------------------------------- */
@theme {
  /* Light-mode "newsprint" values */
  --color-paper: #f7f4ec;
  --color-paper-dark: #ece7da;
  --color-ink: #18130f;
  --color-ink-muted: #5a544b;

  /* Accents */
  --color-newsred: #8c1d11;
  --color-newsred-bright: #b3261a;
  --color-newsgold: #b8860b;
  --color-newsgold-bright: #e0a91b;
  --color-newsblue: #1c3f60;

  /* Typography */
  --font-headline: 'Playfair Display', Georgia, 'Times New Roman', serif;
  --font-body: 'Source Serif 4', Georgia, serif;
  --font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono: 'Special Elite', 'Courier New', monospace;

  /* Motion + elevation */
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --shadow-hard: 4px 4px 0 0 rgb(24 19 15);
}

/* 3. SEMANTIC TOKENS (light/dark aware) ------------------------------------- */
@layer base {
  :root {
    --surface: var(--color-paper);
    --surface-raised: #ffffff;
    --surface-sunken: var(--color-paper-dark);
    --rule: color-mix(in srgb, var(--color-ink) 85%, transparent);
    --rule-soft: color-mix(in srgb, var(--color-ink) 18%, transparent);
    --text: var(--color-ink);
    --text-muted: var(--color-ink-muted);
    --accent: var(--color-newsred);
    --accent-contrast: #ffffff;
    --overlay: rgb(24 19 15 / 0.72);
    color-scheme: light;
  }

  .dark {
    --surface: #12100e;
    --surface-raised: #1b1815;
    --surface-sunken: #0c0a09;
    --rule: color-mix(in srgb, #f7f4ec 82%, transparent);
    --rule-soft: color-mix(in srgb, #f7f4ec 16%, transparent);
    --text: #f2ede2;
    --text-muted: #a9a196;
    --accent: #ff6b5b;
    --accent-contrast: #1a0d0b;
    --overlay: rgb(0 0 0 / 0.8);
    color-scheme: dark;
  }

  * {
    border-color: var(--rule-soft);
  }

  html {
    scroll-behavior: smooth;
    -webkit-text-size-adjust: 100%;
  }

  body {
    background-color: var(--surface);
    color: var(--text);
    font-family: var(--font-body);
    transition:
      background-color 280ms var(--ease-out-expo),
      color 280ms var(--ease-out-expo);
  }

  /* One consistent, high-contrast focus ring for every interactive element. */
  :where(a, button, input, select, textarea, [tabindex]):focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: 2px;
  }

  input,
  select,
  textarea {
    font-family: var(--font-sans);
    color: var(--text);
    background-color: var(--surface-raised);
  }

  input::placeholder,
  textarea::placeholder {
    color: color-mix(in srgb, var(--text-muted) 75%, transparent);
  }
}

/* 4. CORE COMPONENTS --------------------------------------------------------- */
@layer components {
  /* --- Surfaces --- */
  .surface-paper {
    background-color: var(--surface);
    color: var(--text);
  }
  .surface-raised {
    background-color: var(--surface-raised);
    color: var(--text);
  }
  .surface-sunken {
    background-color: var(--surface-sunken);
    color: var(--text);
  }

  .rule {
    border-color: var(--rule);
  }
  .rule-soft {
    border-color: var(--rule-soft);
  }
  .ink {
    color: var(--text);
  }
  .ink-muted {
    color: var(--text-muted);
  }
  .accent-text {
    color: var(--accent);
  }
  /* Headline links: accent on hover, ink at rest. A component class rather
     than `hover:accent-text` because variants cannot target @layer components. */
  .text-link {
    color: var(--text);
    transition: color 180ms ease;
  }
  .text-link:hover,
  .text-link:focus-visible {
    color: var(--color-newsred);
  }
  .dark .text-link:hover,
  .dark .text-link:focus-visible {
    color: var(--color-newsgold-bright);
  }
  .scrim {
    background-color: var(--overlay);
  }

  /* Letterpress-style hard shadow that inverts for dark mode. */
  .press-shadow {
    box-shadow: 4px 4px 0 0 var(--color-ink);
  }
  .dark .press-shadow {
    box-shadow: 4px 4px 0 0 #000;
  }

  /* --- Panels / cards --- */
  .panel {
    border: 1px solid var(--rule);
    background-color: var(--surface);
    border-radius: 0.5rem;
  }
  .panel-raised {
    border: 1px solid var(--rule);
    background-color: var(--surface-raised);
    border-radius: 0.5rem;
  }
  .panel-sunken {
    border: 1px solid var(--rule-soft);
    background-color: var(--surface-sunken);
    border-radius: 0.5rem;
  }

  /* --- Form fields (auth modal + every admin form) --- */
  .field {
    width: 100%;
    border-radius: 0.375rem;
    border: 1px solid var(--rule-soft);
    background-color: var(--surface-raised);
    padding: 0.55rem 0.75rem;
    font-size: 0.8125rem;
    color: var(--text);
    outline: none;
    transition:
      border-color 160ms ease,
      box-shadow 160ms ease;
  }
  .field:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
  }
  .field-label {
    display: block;
    margin-bottom: 0.3rem;
    font-family: var(--font-sans);
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
  }

  /* --- Buttons --- */
  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.45rem;
    border-radius: 0.25rem;
    padding: 0.5rem 0.9rem;
    font-family: var(--font-sans);
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    white-space: nowrap;
    cursor: pointer;
    border: 1px solid transparent;
    transition:
      background-color 180ms ease,
      color 180ms ease,
      border-color 180ms ease,
      transform 180ms ease;
  }
  .btn:active {
    transform: translateY(1px);
  }
  .btn:disabled {
    opacity: 0.55;
    cursor: not-allowed;
  }

  .btn-primary {
    background-color: var(--color-newsred);
    color: #fff;
  }
  .dark .btn-primary {
    background-color: var(--color-newsred-bright);
  }
  .btn-primary:hover:not(:disabled) {
    background-color: var(--color-ink);
  }
  .dark .btn-primary:hover:not(:disabled) {
    background-color: #fff;
    color: #12100e;
  }

  .btn-accent {
    background-color: var(--accent);
    color: var(--accent-contrast);
  }
  .btn-accent:hover:not(:disabled) {
    filter: brightness(1.08);
  }

  .btn-ghost {
    background-color: transparent;
    color: var(--text);
    border-color: var(--rule-soft);
  }
  .btn-ghost:hover:not(:disabled) {
    background-color: var(--surface-sunken);
    border-color: var(--rule);
  }

  .btn-quiet {
    background-color: transparent;
    color: var(--text-muted);
    padding-inline: 0.5rem;
  }
  .btn-quiet:hover:not(:disabled) {
    color: var(--accent);
  }

  /* Destructive actions. Kept visually distinct from every other button so a
     reset can never be triggered by muscle memory. */
  .btn-danger {
    background-color: transparent;
    color: var(--color-newsred);
    border-color: color-mix(in srgb, var(--color-newsred) 55%, transparent);
  }
  .btn-danger:hover:not(:disabled) {
    background-color: var(--color-newsred);
    color: #fff;
    border-color: var(--color-newsred);
  }
  .dark .btn-danger {
    color: var(--color-newsred-bright);
    border-color: color-mix(in srgb, var(--color-newsred-bright) 55%, transparent);
  }
  .dark .btn-danger:hover:not(:disabled) {
    color: #12100e;
  }

  /* --- Badges --- */
  .badge {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    border-radius: 0.25rem;
    padding: 0.15rem 0.45rem;
    font-family: var(--font-mono);
    font-size: 0.625rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    border: 1px solid transparent;
  }
  .badge-live {
    background-color: var(--color-newsred);
    color: #fff;
  }
  .dark .badge-live {
    background-color: var(--color-newsred-bright);
  }
  .badge-gold {
    background-color: color-mix(in srgb, var(--color-newsgold) 22%, transparent);
    color: var(--color-newsgold);
    border-color: color-mix(in srgb, var(--color-newsgold) 45%, transparent);
  }
  .dark .badge-gold {
    color: var(--color-newsgold-bright);
  }
  .badge-neutral {
    background-color: var(--surface-sunken);
    color: var(--text-muted);
    border-color: var(--rule-soft);
  }
  .badge-emerald {
    background-color: color-mix(in srgb, #10b981 18%, transparent);
    color: #047857;
    border-color: color-mix(in srgb, #10b981 40%, transparent);
  }
  .dark .badge-emerald {
    color: #34d399;
  }
  .badge-amber {
    background-color: color-mix(in srgb, #f59e0b 18%, transparent);
    color: #b45309;
    border-color: color-mix(in srgb, #f59e0b 40%, transparent);
  }
  .dark .badge-amber {
    color: #fcd34d;
  }
  .badge-sky {
    background-color: color-mix(in srgb, #0ea5e9 18%, transparent);
    color: #0369a1;
    border-color: color-mix(in srgb, #0ea5e9 40%, transparent);
  }
  .dark .badge-sky {
    color: #7dd3fc;
  }

  /* --- Skip link (keyboard users land here first) --- */
  .skip-link {
    position: absolute;
    left: 0.5rem;
    top: -6rem;
    z-index: 100;
    background-color: var(--color-ink);
    color: var(--color-paper);
    padding: 0.65rem 1rem;
    font-family: var(--font-sans);
    font-size: 0.75rem;
    font-weight: 700;
    transition: top 200ms var(--ease-out-expo);
  }
  .skip-link:focus {
    top: 0.5rem;
  }

  /* Themed scrollable region (search results, admin lists). */
  .scroll-area {
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--rule-soft) transparent;
  }
}

/* 5. DIALOGS, TOASTS, SWITCHES, NAV ---------------------------------------- */
@layer components {
  /* --- Modal shell --- */
  .modal-backdrop {
    position: fixed;
    inset: 0;
    z-index: 60;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    background-color: var(--overlay);
    backdrop-filter: blur(6px);
  }
  .modal-card {
    width: 100%;
    max-height: 92vh;
    overflow-y: auto;
    border: 1px solid var(--rule);
    background-color: var(--surface-raised);
    color: var(--text);
    border-radius: 0.75rem;
    box-shadow: 0 25px 60px -12px rgb(0 0 0 / 0.55);
  }

  /* --- Toast stack --- */
  .toast-stack {
    position: fixed;
    right: 1rem;
    bottom: 1rem;
    z-index: 80;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    width: min(22rem, calc(100vw - 2rem));
  }
  .toast {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    padding: 0.7rem 0.85rem;
    border: 1px solid var(--rule);
    border-left: 3px solid var(--accent);
    border-radius: 0.5rem;
    background-color: var(--surface-raised);
    color: var(--text);
    font-family: var(--font-sans);
    font-size: 0.75rem;
    line-height: 1.4;
    box-shadow: 0 12px 28px -10px rgb(0 0 0 / 0.5);
  }
  .toast-success {
    border-left-color: #10b981;
  }
  .toast-error {
    border-left-color: #ef4444;
  }

  /* --- Toggle switch (dark mode + admin switches) --- */
  .switch {
    position: relative;
    display: inline-flex;
    align-items: center;
    cursor: pointer;
  }
  .switch input {
    position: absolute;
    inset: 0;
    opacity: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    cursor: pointer;
  }
  .switch-track {
    width: 2.75rem;
    height: 1.5rem;
    border-radius: 9999px;
    background-color: color-mix(in srgb, var(--text-muted) 35%, transparent);
    border: 1px solid var(--rule-soft);
    transition: background-color 220ms var(--ease-out-expo);
  }
  .switch-thumb {
    position: absolute;
    top: 3px;
    left: 3px;
    height: 1.125rem;
    width: 1.125rem;
    border-radius: 9999px;
    background-color: var(--surface-raised);
    box-shadow: 0 1px 3px rgb(0 0 0 / 0.35);
    transition: transform 260ms var(--ease-out-expo);
  }
  .switch input:checked ~ .switch-track {
    background-color: var(--color-newsred);
  }
  .dark .switch input:checked ~ .switch-track {
    background-color: var(--color-newsgold);
  }
  .switch input:checked ~ .switch-thumb {
    transform: translateX(1.25rem);
  }
  .switch input:focus-visible ~ .switch-track {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
  }

  /* --- Masthead nav underline --- */
  .nav-link {
    position: relative;
    display: inline-block;
    padding-block: 0.25rem;
    color: var(--text);
    transition: color 180ms ease;
  }
  .nav-link::after {
    content: '';
    position: absolute;
    left: 0;
    bottom: 0;
    height: 2px;
    width: 100%;
    background-color: var(--color-newsred);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform 240ms var(--ease-out-expo);
  }
  .nav-link:hover,
  .nav-link:focus-visible {
    color: var(--color-newsred);
  }
  .nav-link:hover::after,
  .nav-link:focus-visible::after {
    transform: scaleX(1);
  }
  .dark .nav-link:hover,
  .dark .nav-link:focus-visible {
    color: var(--color-newsgold-bright);
  }
  .dark .nav-link::after {
    background-color: var(--color-newsgold-bright);
  }
}

/* 6. OWNER CONTROL CENTER SHELL --------------------------------------------- */
@layer components {
  .admin-shell {
    display: flex;
    flex-direction: column;
    height: 100vh;
    height: 100dvh;
    overflow: hidden;
    background-color: var(--surface-sunken);
  }
  .admin-topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    padding: 0.75rem 1rem;
    border-bottom: 1px solid var(--rule);
    background-color: var(--surface);
    position: sticky;
    top: 0;
    z-index: 30;
  }
  .admin-topbar__left,
  .admin-topbar__right {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
  }

  /* Sidebar + scrolling content: stacked on mobile, side by side on desktop. */
  .admin-layout {
    display: flex;
    flex: 1;
    flex-direction: column;
    min-width: 0;
    min-height: 0;
  }
  .admin-nav {
    border-bottom: 1px solid var(--rule);
    background-color: var(--surface);
    padding: 0.6rem 0.75rem;
  }
  .admin-nav__list {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    gap: 0.35rem;
  }

  /*
  MOBILE NAVIGATION
  .admin-tab is width:100% so it fills the desktop sidebar column. On a phone
  that rule made all ten tabs stack as full-width rows, pushing the actual
  panel content off-screen. Below the desktop breakpoint the tabs become
  auto-width chips that wrap, and the strip scrolls horizontally so it can
  never eat more than a couple of lines of viewport height.
  */
  @media (max-width: 63.999rem) {
    .admin-nav {
      padding: 0.5rem 0.75rem;
      max-height: 7rem;
      overflow-y: auto;
      overscroll-behavior: contain;
    }
    .admin-nav__list {
      flex-wrap: wrap;
      overflow-x: auto;
      overscroll-behavior-x: contain;
      scrollbar-width: thin;
      /* Fade the right edge so it reads as scrollable. */
      mask-image: linear-gradient(
        to right,
        #000 0,
        #000 calc(100% - 1.5rem),
        transparent 100%
      );
    }
    .admin-tab {
      width: auto;
      flex: 0 0 auto;
      white-space: nowrap;
      padding: 0.4rem 0.7rem;
    }

    /*
    PHONE: SCROLL THE PAGE, DON'T LOCK IT
    The desktop shell pins itself to 100dvh and scrolls an inner pane, with the
    document body locked. That works on iOS but is hostile on Android: the URL
    bar collapse animates the dynamic viewport *while* the body is
    overflow:hidden, so the shell ends up taller than the visible area with the
    bottom unreachable. `overscroll-behavior: contain` compounds it by stopping
    the scroll chain from escaping to the document.

    On phones we therefore unlock the body, let the shell grow to its content,
    and hand scrolling back to the browser. Sticky bars keep the controls
    reachable, and the tab strip collapses to a single scrollable row so it can
    never occupy a meaningful slice of the screen.
    */
    .admin-layout {
      min-height: 0;
    }
    .admin-body {
      /* No inner scroller on a phone - the document is the scroller. */
      overflow-y: visible;
      overscroll-behavior: auto;
      /* Anchor scrolling must clear the sticky bars. */
      scroll-margin-top: 1rem;
    }
    /* The remaining phone overrides (document scroll lock, shell height, sticky
       nav and the single-row tab strip) are declared at the end of this layer,
       after the desktop rules they need to beat. */
  }
  @media (min-width: 64rem) {
    .admin-layout {
      flex-direction: row;
      /* Both columns must stretch to the row height. `flex-start` would let
         .admin-body size to its content and clip it against the shell. */
      align-items: stretch;
    }
    .admin-body {
      /* Scroll the panel itself instead of the locked page body. */
      align-self: stretch;
      height: 100%;
    }
    .admin-nav {
      width: 15rem;
      flex-shrink: 0;
      align-self: stretch;
      border-right: 1px solid var(--rule);
      border-bottom: none;
      padding: 1rem 0.75rem;
      /* The sidebar can hold more tabs than fit, so it scrolls on its own. */
      overflow-y: auto;
      overscroll-behavior: contain;
    }
    .admin-nav__list {
      flex-direction: column;
      flex-wrap: nowrap;
    }
  }

  .admin-tab {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    width: 100%;
    border-radius: 0.375rem;
    padding: 0.5rem 0.7rem;
    font-family: var(--font-sans);
    font-size: 0.75rem;
    font-weight: 600;
    text-align: left;
    cursor: pointer;
    color: var(--text-muted);
    background-color: transparent;
    border: 1px solid transparent;
    transition:
      background-color 180ms ease,
      color 180ms ease,
      border-color 180ms ease;
  }
  .admin-tab:hover {
    background-color: var(--surface-sunken);
    color: var(--text);
  }
  .admin-tab.is-active,
  .admin-tab[aria-current='page'] {
    background-color: var(--color-newsred);
    border-color: var(--color-newsred);
    color: #fff;
  }
  .dark .admin-tab.is-active,
  .dark .admin-tab[aria-current='page'] {
    background-color: var(--color-newsred-bright);
    border-color: var(--color-newsred-bright);
    color: #fff;
  }

  .admin-body {
    flex: 1;
    min-width: 0;
    min-height: 0;
    padding: 1rem;
    /* The page body is scroll-locked while the workspace is open, so the
       workspace content must scroll here or tall tabs become unreachable. */
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--rule-soft) transparent;
  }
  @media (min-width: 64rem) {
    .admin-body {
      padding: 1.5rem;
    }
  }

  /* Right-hand control cluster in the admin top bar. */
  .admin-theme-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
  }

  /* Applied to the workspace host element while it is mounted. */
  .admin-mode {
    display: block;
  }

  /* The page scroll lock and the full-bleed / hide-public-chrome rules that let
     the workspace own the whole screen now live UNLAYERED, further down this
     file. They used to sit here, inside `@layer components`, and so were
     silently losing to the Tailwind utility layer: `<main id="main-content">`
     carries `mx-auto max-w-7xl px-4 py-8`, and because `@layer utilities` is
     declared after `@layer components`, the utilities won on layer order no
     matter how specific the selector was. A 100dvh shell therefore sat inside a
     padded, width-capped box, below the fold, in a body locked with
     `overflow: hidden` — so the Owner panel could not be scrolled at all.
     See "OWNER WORKSPACE LAYOUT OVERRIDES" below. */

  /*
    HORIZONTAL SCALING
    Long titles, emails and URLs in tables were stretching the flex/grid
    children instead of wrapping, so the panel grew past the viewport. The
    rules below let those cells shrink and wrap, and keep the scroll region on
    the body rather than the page.
  */
  .admin-body {
    overflow-x: hidden;
  }
  /*
    Tables that the views already wrap in `.overflow-x-auto` keep the default
    auto layout so they can scroll; only loose tables are pinned to the
    container width, otherwise `table-layout: fixed` would squeeze their
    columns into unreadable slivers.
  */
  .admin-body table:not(.overflow-x-auto table) {
    table-layout: fixed;
    width: 100%;
  }
  .admin-body td,
  .admin-body th {
    /* Without this, a long unbroken string sets the column's minimum width. */
    overflow-wrap: anywhere;
    word-break: break-word;
  }
  .admin-body img {
    max-width: 100%;
    height: auto;
  }

  /* Dialogs opened from the workspace must not exceed the viewport either. */
  .admin-shell dialog {
    max-width: min(56rem, calc(100vw - 2rem));
  }

  /*
  PHONE TUNING
  Wider tables stop trying to squeeze into a phone viewport and scroll
  sideways inside their own container instead, so a single column of data is
  never compressed to an unreadable sliver.
  */
  @media (max-width: 47.999rem) {
    .admin-topbar {
      padding: 0.6rem 0.75rem;
      gap: 0.5rem;
    }
    .admin-body {
      padding: 0.75rem;
    }
    .admin-body table {
      /* Undo the desktop `fixed` layout: real auto layout + a min width lets
         the table scroll horizontally rather than crush its columns. */
      table-layout: auto;
      min-width: 34rem;
    }
    /* Tables the views already wrap get their own scroll container. */
    .admin-body .overflow-x-auto {
      overflow-x: auto;
      -webkit-overflow-scrolling: touch;
    }
  }

  /*
  PHONE: UNLOCK THE DOCUMENT AND SHRINK THE TAB STRIP
  This block is last in the layer on purpose. The desktop scroll lock and the
  tab-strip rules above share this layer and specificity, so the last
  declaration is the one that applies — these phone overrides have to come
  after them to take effect at all.

  Two problems it solves, both reported on Android but harmless on iOS:

  1. The desktop design pins the shell to 100dvh and scrolls an inner pane.
     Android animates the dynamic viewport as its URL bar collapses/expands,
     so a body with `overflow: hidden` ends up with content below the fold that
     nothing can reach. Here the document becomes the scroller again.

  2. Ten tabs wrapped onto four rows and swallowed half the screen. The list is
     one horizontally-scrolling row, so it can never take more than a line.
  */
  @media (max-width: 63.999rem) {
    body.admin-active {
      overflow: auto;
      overscroll-behavior-y: contain;
    }
    .admin-shell {
      height: auto;
      min-height: 100svh;
      overflow: visible;
    }
    .admin-body {
      overflow-y: visible;
      overscroll-behavior: auto;
    }
    .admin-nav {
      position: sticky;
      top: 0;
      z-index: 20;
      max-height: none;
      overflow: visible;
      padding: 0.4rem 0.6rem;
    }
    .admin-nav__list {
      display: flex;
      flex-direction: row;
      flex-wrap: nowrap;
      gap: 0.35rem;
      overflow-x: auto;
      overscroll-behavior-x: contain;
      -webkit-overflow-scrolling: touch;
      scrollbar-width: none;
    }
    .admin-nav__list::-webkit-scrollbar {
      display: none;
    }
    .admin-tab {
      width: auto;
      flex: 0 0 auto;
      white-space: nowrap;
      padding: 0.35rem 0.65rem;
      font-size: 0.7rem;
    }
  }
}

/* =============================================================================
   OWNER WORKSPACE LAYOUT OVERRIDES  —  DELIBERATELY OUTSIDE EVERY @layer
   -----------------------------------------------------------------------------
   These rules are unlayered on purpose. `@layer components` is declared before
   `@layer utilities` in this file, and Tailwind's own layer order means a
   utility ALWAYS beats a component regardless of how specific the component
   selector is. `<main id="main-content">` carries `mx-auto max-w-7xl px-4 py-8`,
   so the `padding: 0` / `max-width: none` below used to be applied inside the
   components layer and silently discarded. Unlayered rules sit above all layers,
   so they now actually take effect.

   The bug they fix: the shell is a 100dvh app shell, but it was being rendered
   inside a padded, width-capped <main> that was itself below the site header.
   The document grew to ~1600px inside a 900px viewport while `body` was locked
   with `overflow: hidden`, so the lower half of the Newsroom Panel could
   not be reached by any means — the reported "I can't scroll the owner panel".
   ========================================================================== */

/* LAYER LADDER — declared in one place so the numbers cannot drift apart.

     100  .skip-link
      80  .toast-stack          — toasts stay readable over every dialog
      60  .modal-backdrop       — EVERY dialog, including the gallery lightbox
      50  #main-content while   — the Newsroom Panel
          body.admin-active
       1  .admin-topbar / .admin-nav (inside the 50 context)
       0  reader views, page flow

   The panel sits BELOW 60 on purpose. The dialogs the Owner opens from inside the
   panel are body-level `.modal-backdrop`s, so they must still be able to rise
   above it. What the panel has to beat is the gallery lightbox, and that is
   handled two ways: it is raised into its own stacking context at 50 (above all
   static page flow), AND openAdmin() closes it. Either alone would be enough;
   together, a stale or late-opening lightbox cannot cover the panel. */
body.admin-active #main-content {
  /*
    `position` + `z-index` are the fix for "the gallery stays on top of the
    Owner panel". Without them #main-content is `position: static` with no
    z-index, so it is painted in normal flow BELOW every positioned descendant
    of <body> — and the lightbox is exactly that (`position: fixed;
    z-index: 60`). Raising main into its own stacking context at 50 puts the
    whole panel, including its sticky nav and topbar, above the reader views
    while leaving body-level dialogs free to cover the panel.
  */
  position: relative;
  z-index: 50;
  max-width: none;
  padding: 0;
  width: 100%;
  margin: 0;
  height: 100dvh;
  overflow: hidden;
}

/* Belt and braces: even if a lightbox were reopened by a stale handler while the
   panel is mounted, it must not paint over it. Hiding it outright is safer than
   relying on a higher z-index, because z-index alone cannot win once the two
   elements end up in different stacking contexts. */
body.admin-active #lightbox {
  display: none !important;
}

/* The panel owns the whole viewport, so nothing underneath it is clickable.
   `visibility: hidden` on the reader views additionally stops them being
   reached by Tab if the panel is ever closed by a path that forgets to
   re-show exactly one view. */
body.admin-active #publication-view,
body.admin-active #gallery-view,
body.admin-active #credits-view {
  visibility: hidden;
  pointer-events: none;
}

body.admin-active #admin-view {
  display: block;
  width: 100%;
  max-width: 100%;
  height: 100%;
  overflow: hidden;
}

/* <header> and <footer> are SIBLINGS of <main>, so the full-bleed rules above
   never reached them. Left in flow they wrapped a 100dvh shell and pushed the
   workspace roughly 400px down the page. */
body.admin-active > header,
body.admin-active > footer {
  display: none !important;
}

/* MODAL SCROLL LOCK — set by openDialog() in dom.js. This was once an inline
   `document.body.style.overflow`, which outranks every stylesheet rule, so a
   lock left over from a dialog still open when the workspace mounted could never
   be cleared. As a class it participates in the normal cascade. */
body.admin-active,
body.admin-active.dialog-locked {
  overflow: hidden;
}

/* On a phone the document becomes the scroller again: Android animates the
   dynamic viewport as its URL bar collapses, so an inner-pane scroller plus a
   locked body strands content below the fold. These must mirror the phone
   overrides inside @layer components. */
@media (max-width: 63.999rem) {
  body.admin-active,
  body.admin-active.dialog-locked {
    overflow: auto;
    overscroll-behavior-y: contain;
  }
  body.admin-active #main-content {
    height: auto;
    min-height: 100svh;
    overflow: visible;
  }
  body.admin-active #admin-view {
    height: auto;
    overflow: visible;
  }
}
/* 5b. APP STATE HOOKS -------------------------------------------------------- */

@layer components {
  /* Auth modal mode tabs (Sign in / Create account). */
  .auth-tab {
    flex: 1;
    border: 1px solid var(--rule-soft);
    background-color: transparent;
    color: var(--text-muted);
  }
  .auth-tab[aria-selected='true'] {
    background-color: var(--color-newsred);
    border-color: var(--color-newsred);
    color: #fff;
  }
  .dark .auth-tab[aria-selected='true'] {
    background-color: var(--color-newsred-bright);
    border-color: var(--color-newsred-bright);
  }

  /* Sun/moon glyph inside the dark-mode switch thumb. */
  .theme-icon {
    line-height: 1;
    transition:
      transform 260ms var(--ease-out-expo),
      opacity 200ms ease;
  }
  .dark .theme-icon {
    color: var(--color-newsgold-bright);
  }

  /* Reveal-on-boot: prevents a flash of unstyled content before hydration. */
  .app-ready #publication-view,
  .app-ready #auth-slot {
    animation: rise 420ms var(--ease-out-expo) both;
  }
}

/* 7. NEWSPAPER DETAIL -------------------------------------------------------- */
@layer components {
  /* Halftone dot field laid over the masthead. */
  .newspaper-texture::before {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    background-image: radial-gradient(currentColor 0.5px, transparent 0.5px);
    background-size: 16px 16px;
    opacity: 0.06;
  }

  /* Classic broadsheet drop cap. */
  .first-letter-cap::first-letter {
    font-family: var(--font-headline);
    font-size: 3.6rem;
    float: left;
    line-height: 0.82;
    padding: 4px 8px 0 0;
    font-weight: 900;
    color: var(--color-newsred);
  }
  .dark .first-letter-cap::first-letter {
    color: var(--color-newsgold-bright);
  }

  /* Washi-tape edging used on pull quotes and memo cards. */
  .tape-accent {
    background: color-mix(in srgb, var(--color-paper-dark) 70%, transparent);
    border-left: 2px dashed color-mix(in srgb, var(--color-ink) 30%, transparent);
    border-right: 2px dashed color-mix(in srgb, var(--color-ink) 30%, transparent);
    box-shadow: 0 1px 3px rgb(0 0 0 / 0.06);
  }
  .dark .tape-accent {
    background: color-mix(in srgb, #2a2520 70%, transparent);
  }
}


/* 7b. PHOTO GALLERY ---------------------------------------------------------- */
@layer components {
  /* Mobile-first: two tight columns on a phone, widening as space allows. Using
     `auto-fit` with a minmax floor means a single photograph still fills the
     row instead of being stranded at one column's width. */
  .gallery-grid {
    display: grid;
    gap: 0.625rem;
    grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  }
  @media (min-width: 40rem) {
    .gallery-grid {
      gap: 0.875rem;
      grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
    }
  }

  .gallery-item {
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 0.375rem;
  }

  /* The whole tile is one button so keyboard and screen-reader users get the
     same affordance as a tap, and the image gets a large hit target. */
  .gallery-btn {
    position: relative;
    display: block;
    width: 100%;
    padding: 0;
    overflow: hidden;
    border: 1px solid var(--rule-soft);
    border-radius: 0.25rem;
    background-color: var(--surface-sunken, transparent);
    cursor: zoom-in;
    /* Reserve the tile's height before the image decodes, so the grid does not
       reflow (and jump the reader's scroll position) as photos arrive. */
    aspect-ratio: 4 / 3;
  }
  .gallery-btn img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 420ms var(--ease-out-expo);
  }

  /* Hover zoom is pointer-only: on touch it would fight the tap, and the
     reduced-motion query below disables the movement for everyone else. */
  @media (hover: hover) and (prefers-reduced-motion: no-preference) {
    .gallery-btn:hover img,
    .gallery-btn:focus-visible img {
      transform: scale(1.06);
    }
  }

  .gallery-btn:focus-visible {
    outline: 2px solid var(--color-newsred);
    outline-offset: 2px;
  }

  .gallery-zoom {
    position: absolute;
    right: 0.375rem;
    bottom: 0.375rem;
    display: grid;
    place-items: center;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 9999px;
    background-color: rgb(0 0 0 / 0.55);
    color: #fff;
    font-size: 0.75rem;
    opacity: 0;
    transition: opacity 200ms ease;
  }
  @media (hover: hover) {
    .gallery-btn:hover .gallery-zoom,
    .gallery-btn:focus-visible .gallery-zoom {
      opacity: 1;
    }
  }

  .gallery-item figcaption {
    font-size: 0.6875rem;
    line-height: 1.35;
    color: var(--text-muted);
  }
}

/* 8. UTILITIES + MOTION ------------------------------------------------------ */
@layer utilities {
  .animate-modal-in {
    animation: modalIn 320ms var(--ease-out-expo) both;
  }
  .animate-backdrop-in {
    animation: backdropIn 220ms ease both;
  }
  .animate-toast-in {
    animation: toastIn 280ms var(--ease-out-expo) both;
  }
  .animate-rise {
    animation: rise 480ms var(--ease-out-expo) both;
  }
  .emergency-pulse {
    animation: pulseGlow 2.5s infinite ease-in-out;
  }
  .spin-slow {
    animation: spin 1.1s linear infinite;
  }
}

/* Keyframes live OUTSIDE @layer — the CSS minifier treats a nested
   @keyframes as an unknown at-rule and silently drops it. */
@keyframes modalIn {
  from {
    opacity: 0;
    transform: translateY(18px) scale(0.97);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}
@keyframes backdropIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
@keyframes toastIn {
  from {
    opacity: 0;
    transform: translateX(16px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}
@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
@keyframes pulseGlow {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.82;
  }
}
@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* Themed scrollbars — not hard-coded, so they follow the theme. */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
::-webkit-scrollbar-track {
  background: transparent;
}
::-webkit-scrollbar-thumb {
  background: var(--rule-soft);
  border-radius: 9999px;
}
::-webkit-scrollbar-thumb:hover {
  background: var(--rule);
}

/* Accessibility: honour the OS "reduce motion" setting everywhere. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Print: the publication should come out like actual newsprint. */
@media print {
  .no-print {
    display: none !important;
  }
  body {
    background: #fff;
    color: #000;
  }
}

/* ==========================================================================
   Byline portraits ("sticker" style)
   --------------------------------------------------------------------------
   An approved staff selfie rendered as a die-cut sticker beside a byline or
   on a Credits card. The white ring + shadow imitate a trimmed photograph, and
   a slight rotation keeps a column of them from looking mechanical.
   ========================================================================== */

.byline-sticker {
  display: block;
  width: 2.25rem;
  height: 2.25rem;
  flex-shrink: 0;
  border-radius: 9999px;
  object-fit: cover;
  object-position: center 30%;
  background: var(--panel-2, #f1f1ef);
  /* the die-cut edge */
  border: 2px solid #fff;
  box-shadow:
    0 1px 2px rgb(0 0 0 / 0.18),
    0 4px 10px rgb(0 0 0 / 0.12);
  transform: rotate(-3deg);
  transition: transform 0.18s ease;
}

.byline-sticker:hover {
  transform: rotate(0deg) scale(1.06);
}

/* Credits card size. */
.byline-sticker-lg {
  width: 6.5rem;
  height: 6.5rem;
  border-width: 3px;
  box-shadow:
    0 2px 4px rgb(0 0 0 / 0.2),
    0 10px 24px rgb(0 0 0 / 0.16);
}

/* Placeholder shown when a portrait is still awaiting Owner approval. */
.byline-sticker-empty {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-muted, #6b6b6b);
  background: var(--panel-2, #ececea);
  border-style: dashed;
  border-color: var(--rule, #d6d6d2);
  box-shadow: none;
  transform: none;
}

/* The portrait may be tapped to open a full-size view. */
button.byline-sticker {
  cursor: zoom-in;
  padding: 0;
  border-color: #fff;
}

/* A byline: portrait + name + tail, on one line. */
.byline-sticker-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
}

.byline-sticker-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .byline-sticker,
  .byline-sticker:hover {
    transition: none;
    transform: none;
  }
}
/* =============================================================================
   GALLERY PAGE + ARTICLE PHOTO STRIP
   -----------------------------------------------------------------------------
   Added for the dedicated gallery page (Owner-defined category cards) and for
   the supporting photos that sit under a dispatch's body copy.

   Motion policy: the card expansion is animated, but only under
   `prefers-reduced-motion: no-preference`, and it animates max-height and
   opacity rather than a transform, because an expanding grid has to push the
   rest of the page down and a slide would desynchronise from that reflow.
   ========================================================================== */

/* -----------------------------------------------------------------------------
   Category page (level 2 of the gallery)

   A category card used to expand a grid underneath itself. It now navigates: the
   card is a button that swaps the whole view for a page of that category's
   photographs, with a Back button. The transition is a short slide-and-fade so
   the reader reads it as going somewhere rather than as a disclosure opening,
   and it is entirely under `prefers-reduced-motion: no-preference`.
   -------------------------------------------------------------------------- */

@keyframes galleryPageIn {
  from {
    opacity: 0;
    transform: translateX(2.5rem);
  }

  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@media (prefers-reduced-motion: no-preference) {
  .gallery-category-page {
    animation: galleryPageIn 240ms cubic-bezier(0.2, 0, 0, 1) both;
  }
}

.gallery-category-page__back {
  margin-bottom: 0.75rem;
}

/* -----------------------------------------------------------------------------
   Category cards
   -------------------------------------------------------------------------- */

.gallery-cards {
  display: grid;
  gap: 1rem;
}

@media (min-width: 48rem) {
  .gallery-cards {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.gallery-card {
  border: 1px solid color-mix(in srgb, currentColor 12%, transparent);
  background: var(--paper-raised, Canvas);
}

.gallery-card__head {
  display: flex;
  width: 100%;
  cursor: pointer;
  align-items: center;
  gap: 0.875rem;
  padding: 0.75rem;
  text-align: left;
}

.gallery-card__cover {
  width: 4.5rem;
  height: 3.5rem;
  flex: 0 0 auto;
  object-fit: cover;
}

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

/* The chevron points forward, and nudges on hover: the card navigates. */
.gallery-card__chevron {
  flex: 0 0 auto;
}

@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .gallery-card__head:hover .gallery-card__chevron {
    transform: translateX(0.25rem);
  }

  .gallery-card__chevron {
    transition: transform 220ms ease;
  }
}

/* Photographs on a category page. Deliberately a single column on a phone: a
   two-up grid of 4:3 crops on a 360px screen is ~160px of detail per shot, which
   is smaller than the thumb you tap to open the lightbox. */
.gallery-card__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.625rem;
}

@media (min-width: 40rem) {
  .gallery-card__grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.gallery-card__shot {
  display: block;
  width: 100%;
  cursor: pointer;
  overflow: hidden;
  background: color-mix(in srgb, currentColor 6%, transparent);
  text-align: left;
}

.gallery-card__shot img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

.gallery-card__caption {
  display: block;
  padding: 0.375rem 0.5rem;
  font-size: 0.6875rem;
  line-height: 1.3;
  color: var(--ink-muted, currentColor);
}

@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .gallery-card__shot:hover img {
    transform: scale(1.05);
  }

  .gallery-card__shot img {
    transition: transform 260ms ease;
  }
}

/* -----------------------------------------------------------------------------
   Article photo strip
   -------------------------------------------------------------------------- */

.article-photos__toggle {
  display: flex;
  width: 100%;
  cursor: pointer;
  align-items: center;
  gap: 0.75rem;
  border: 1px solid color-mix(in srgb, currentColor 12%, transparent);
  padding: 0.5rem;
  text-align: left;
}

.article-photos__thumbs {
  display: flex;
  flex: 1 1 auto;
  gap: 0.375rem;
  min-width: 0;
}

.article-photos__thumbs img {
  width: 3.75rem;
  height: 3rem;
  flex: 0 0 auto;
  object-fit: cover;
}

.article-photos__label {
  flex: 0 0 auto;
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted, currentColor);
}

/* One photo per row on a phone. Two side by side at 4:3 is the widest that
   still leaves each thumbnail legible at arm's length. */
.article-photos__grid {
  display: grid;
  gap: 0.625rem;
  margin-top: 0.625rem;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

@media (min-width: 48rem) {
  .article-photos__grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.article-photos__item {
  margin: 0;
  overflow: hidden;
  background: color-mix(in srgb, currentColor 6%, transparent);
}

.article-photos__item img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

@media (prefers-reduced-motion: reduce) {
  .gallery-category-page {
    animation: none;
  }

  .gallery-card__chevron,
  .gallery-card__shot img {
    transition: none;
    transform: none;
  }
}

/* --------------------------------------------------------------------------
   Credits role bands
   --------------------------------------------------------------------------
   Discord's member list has one coloured block per role and the blocks stack.
   That is the model here: the Owner orders ROLES, not 40 individual people, and
   everyone holding a role moves as one unit.

   `--band` is set inline per band by renderCredits() from the role's own colour.
   color-mix() derives the soft fill from it, so one colour drives the whole
   block and a band can never disagree with the members inside it.
   -------------------------------------------------------------------------- */

/* The band itself is a plain section: no fill, no border, no radius. It used to
   be a tinted rounded box wrapping every role, which on a phone became one very
   long coloured column with the cards stacked inside it. */
.credits-band {
  --band: #c8102e;
  margin-bottom: 2rem;
}

.credits-band:last-child {
  margin-bottom: 0;
}

.credits-band__head {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  margin-bottom: 0.875rem;
}

/* The rail is the band identity: a short colour stripe on the LEFT of the role
   name, echoing the Discord role chip. */
.credits-band__rail {
  flex: 0 0 auto;
  width: 0.25rem;
  align-self: stretch;
  min-height: 1.125rem;
  background: var(--band);
}

.credits-band__name {
  font-family: var(--font-headline, inherit);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--band) 72%, var(--color-ink, #111));
}

/* The count sits on the FAR RIGHT of the row, which is what `margin-left: auto`
   buys inside a flex container. */
.credits-band__count {
  margin-left: auto;
  flex: 0 0 auto;
  min-width: 1.5rem;
  padding: 0.0625rem 0.5rem;
  border: 1px solid color-mix(in srgb, var(--band) 40%, transparent);
  border-radius: 999px;
  font-size: 0.6875rem;
  font-weight: 700;
  text-align: center;
  color: color-mix(in srgb, var(--band) 70%, var(--color-ink, #111));
}

/* One / md:two / lg:three, with a 1rem gap. Named on the <ul> rather than reusing
   the old credits-band__grid so the Owner panel's editor grid cannot inherit it. */
.credits-grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr;
  margin: 0;
  padding: 0;
  list-style: none;
}

@media (width >= 48rem) {
  .credits-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (width >= 64rem) {
  .credits-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

/* --- member card ----------------------------------------------------------- */

/* Sharp corners, thin border, plain background, everything left-aligned. The
   hover/focus treatment is a hard offset shadow rather than a colour change, so
   it reads as a pressed key and cannot be missed by a keyboard user. */
.credits-card {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1rem;
  border: 1px solid var(--color-neutral-700, #404040);
  border-radius: 0;
  background: transparent;
  transition:
    box-shadow 120ms ease,
    border-color 120ms ease;
}

.credits-card:hover,
.credits-card:focus-within {
  border-color: var(--band, var(--color-newsred, #c8102e));
  box-shadow: 4px 4px 0 0 var(--band, var(--color-newsred, #c8102e));
}

.credits-card__avatar {
  flex: 0 0 auto;
  width: 3rem;
  height: 3rem;
  border-radius: 999px;
  overflow: hidden;
  object-fit: cover;
  background: var(--color-neutral-800, #262626);
}

.credits-card__avatar--empty {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--color-neutral-400, #a3a3a3);
}

.credits-card__body {
  min-width: 0;
  text-align: left;
}

.credits-card__name {
  font-family: var(--font-headline, inherit);
  font-size: 1rem;
  font-weight: 700;
  text-align: left;
  color: var(--color-ink, inherit);
}

.credits-card__blurb {
  margin-top: 0.25rem;
  font-size: 0.75rem;
  line-height: 1.4;
  text-align: left;
  color: var(--color-neutral-400, #a3a3a3);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  .credits-card {
    transition: none;
  }
}

/* --- Owner panel: the reorderable role bands ------------------------------ */

/* One draggable-looking block per role in the Credits tab. The border picks up
   the role colour so the Owner recognises the same band they see on the page. */
.credits-band-editor {
  --band: #c8102e;
  margin-bottom: 1.5rem;
  border-radius: 0.875rem;
  border: 1px solid color-mix(in srgb, var(--band) 30%, transparent);
  background: color-mix(in srgb, var(--band) 5%, transparent);
  padding: 0.75rem;
}

.credits-band-editor__head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
  padding: 0.25rem;
}

.credits-band-editor__name {
  font-family: var(--font-headline, inherit);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--band) 70%, var(--color-ink, #111));
}

.credits-band-editor__count {
  font-size: 0.6875rem;
  color: var(--color-ink-muted, #666);
}

/* Move buttons sit on the right on desktop, and wrap to a full-width pair on a
   phone, where a 32px target is too small to hit reliably. */
.credits-band-editor__moves {
  margin-left: auto;
  display: flex;
  gap: 0.25rem;
}

@media (width < 30rem) {
  .credits-band-editor__moves {
    margin-left: 0;
    width: 100%;
  }

  .credits-band-editor__moves > button {
    flex: 1 1 0;
    justify-content: center;
  }
}
