/* ==========================================================================
   Design tokens

   Brand anchors (bg / accent / text) are fixed by brand guidelines — every
   other color here is derived from them, not picked independently. Type is
   a dependency-free system stack on purpose: this is an internal tool that
   needs to keep working untouched for years, so no webfont request that
   could someday 404. Distinction comes from disciplined use of a monospace
   face for anything data-shaped (slugs, keys, ids, timestamps, counts) —
   see .mono / .eyebrow below, used consistently everywhere in the admin.
   ========================================================================== */

:root {
    --bg: #0A0A12;
    --surface: #12121C;
    --surface-2: #16161F;
    --surface-3: #1C1C27;
    --surface-elevated: #181822;
    --accent: #2fa711;
    --accent-hover: #34b814;
    --accent-soft: rgba(76, 183, 49, 0.12);
    --accent-subtle: rgba(76, 183, 49, 0.035);
    --accent-subtle-strong: rgba(76, 183, 49, 0.07);
    --text: #FFFFFF;
    --text-muted: #9A9AAD;
    --text-faint: #5C5C6E;
    --border: #24242E;
    --border-strong: #34343F;
    --border-focus: rgba(76, 183, 49, 0.7);
    /* A genuine red, not the coral/salmon #E8564A previously here — that
       read as pink rather than a clear "destructive action" red. */
    --danger: #D63638;
    --danger-hover: #E15558;
    --danger-soft: rgba(214, 54, 56, 0.12);
    /* Same hue as --nav-pages — reused here as the one "needs attention,
       not broken" warning tone (e.g. a stale translation), distinct from
       both --accent (good) and --danger (bad/destructive). */
    --warning: #E0B24F;
    --warning-soft: rgba(224, 178, 79, 0.12);

    /* One resting color per sidebar item, so the icon rail reads at a
       glance before you've even parsed the label. All muted to the same
       saturation/lightness so none of them shout — brand green is
       deliberately excluded here, since it stays reserved for the
       active/hover state, and trash reuses --danger since it's already
       the app's "destructive" color. */
    --nav-dashboard: #5B9BD9;
    --nav-content: #6FB1E0;
    --nav-games: #52B788;
    --nav-news: #E0B24F;
    --nav-reviews: #C9695B;
    --nav-guides: #7EC4CF;
    --nav-opinions: #E0824F;
    --nav-deals: #9BA857;
    --nav-upcoming-games: #4FD1A5;
    --nav-media: #C77DFF;
    --nav-categories: #B57BE0;
    --nav-submissions: #E05C87;
    --nav-forms: #6FBF73;
    --nav-redirects: #4FBFC2;
    --nav-seo: #3FA9C9;
    --nav-integrations: #7E8CE0;
    --nav-trash: var(--danger);
    --nav-settings: #8C97A6;
    --nav-maintenance: #D6A74E;
    --nav-ai-context: #5FCFE0;
    --nav-ai-providers: #C9A6E0;
    --nav-import-export: #6B93A8;

    --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
    --font-mono: ui-monospace, "SF Mono", "Cascadia Code", "Consolas", "Roboto Mono", "Segoe UI Symbol", "Segoe UI Emoji", monospace;
    --font-size-sm: 13px;
    --font-size-nav: 15px;

    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 20px;
    --space-6: 24px;
    --space-7: 32px;
    --space-8: 40px;
    --space-9: 48px;
    --space-10: 64px;

    --select-arrow-size: 4px;
    --select-arrow-lift: 1px;

    --radius-sm: 6px;
    --radius-md: 10px;
    --radius-lg: 14px;

    --control-height: 38px;
    --pager-size: 36px;
    --pager-jump-width: 64px;
    --list-search-width: 300px;
    --table-field-min-width: 200px;
    --content-measure: 860px;
    --page-max-width: 1480px;

    --shadow-card: 0 6px 12px rgba(0, 0, 0, 0.4);
    --shadow-focus: 0 0 0 3px rgba(76, 183, 49, 0.12);

    --transition-fast: 120ms ease;

    /* Button tokens — every button/.button variant is built from these, so
       size never varies between variants or on hover, and hover always sets
       both background and text color explicitly (never left to inherit from
       a plain `a:hover` rule, which is what caused green buttons to lose
       their text on hover before). */
    --btn-radius: 8px;
    --btn-padding-y: 8px;
    --btn-padding-x: 14px;
    --btn-font-size: 13px;
    --btn-font-weight: 600;
    --btn-gap: 10px;

    --btn-primary-bg: var(--accent);
    --btn-primary-bg-hover: var(--accent-hover);
    --btn-primary-text: white;
    --btn-primary-text-hover: white;

    --btn-danger-bg: var(--danger);
    --btn-danger-bg-hover: var(--danger-hover);
    --btn-danger-text: #FFFFFF;
    --btn-danger-text-hover: #FFFFFF;

    /* Secondary = "navigate to edit," distinct from Save (green/primary)
       and Delete (red/danger). Reuses the sidebar's Dashboard blue for a
       consistent "view/go" association across the app. */
    --btn-secondary-bg: #4A8FD1;
    --btn-secondary-bg-hover: #5FA3E3;
    --btn-secondary-text: #FFFFFF;
    --btn-secondary-text-hover: #FFFFFF;

    /* Duplicate — distinct from Edit/Save/Delete, so a row with all four
       still reads as four different actions rather than two blues. */
    --btn-duplicate-bg: #4529AF;
    --btn-duplicate-bg-hover: #6A54BF;
    --btn-duplicate-text: #FFFFFF;
    --btn-duplicate-text-hover: #FFFFFF;
}

* { box-sizing: border-box; }

html {
    overscroll-behavior-y: none;
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font-ui);
    font-size: 14px;
    line-height: 1.55;
    overscroll-behavior-y: none;
}

a { color: var(--accent); cursor: pointer; text-decoration: none; transition: color var(--transition-fast); }
a:hover { color: var(--accent-hover); }

/* Keyboard focus is always visible — mouse users never see this ring. */
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
    * { transition-duration: 0.01ms !important; }
}

/* Technical / data-shaped content: slugs, keys, ids, timestamps, counts.
   The one deliberate typographic signature of this UI — everything that
   names or counts something reads in mono with tabular figures, so scanning
   a list of exact values feels like reading an instrument panel, not a
   generic dashboard. */
/* One size step down from body text (15px): monospace glyphs are wider
   per character than the sans-serif UI font at the same nominal size, so
   left at 15px a slug/id reads visibly bigger than a title next to it
   even though neither has an explicit size of its own. */
code, .mono {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 13px;
}

code {
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 1px 5px;
    font-size: 13px;
}

/* Small tracked-uppercase label used above a heading or section — encodes
   "this is a category," nothing more. Used sparingly. */
.eyebrow {
    display: block;
    font-family: var(--font-mono);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-faint);
    margin-bottom: 4px;
}

/* When a .hint follows a field's control directly (same .field div), it
   needs real separation from it — not the label-to-input 4px, that reads
   as attached/broken. --space-3 is the standard gap between a control and
   any secondary content hanging below it (see .upload-row, .thumb-row). */
.hint {
    color: var(--text-muted);
    font-size: 13px;
    line-height: 1.55;
    margin: var(--space-3) 0 0;
}

/* A story's age ("1d ago") on its own line under a list row's text. */
.hint.hint--age {
    color: var(--danger);
}

/* margin-bottom (not the relationship-only margin-top pattern used
   elsewhere in this file) because .empty-state has to carry its own
   trailing space unconditionally — the block that follows it varies
   (nothing, a heading, a whole .card wrapping a "not yet translated"
   table, ...) and can't always be targeted by a specific sibling/child
   selector the way .card > h2 + * can. Same value as .card's own
   margin-bottom, since both play the same "space after this content
   block" role. .card > :is(..., .empty-state):last-child below still
   zeroes this out when an .empty-state is the last thing inside a card,
   so it never doubles up with that card's own bottom padding. */
.empty-state {
    color: var(--text-muted);
    margin-bottom: var(--space-6);
    padding: var(--space-8);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-md);
    background: rgba(255, 255, 255, 0.015);
    text-align: center;
}

/* ==========================================================================
   Layout
   ========================================================================== */

/* align-items defaults to stretch (not overridden here) so .admin-sidebar
   always matches .admin-main's full height — with flex-start, a page
   whose content runs taller than one viewport left the sidebar (and its
   border-right) stopping at 100vh while the page kept scrolling below
   that point, cutting the border off partway down instead of running the
   full length of the page. */
.admin-layout {
    display: flex;
    min-height: 100vh;
}

.admin-sidebar {
    display: flex;
    flex-direction: column;
    width: 210px;
    flex-shrink: 0;
    min-height: 100vh;
    border-right: 1px solid var(--border);
    padding: var(--space-6) 0;
}

.admin-brand {
    display: block;
    font-weight: 700;
    font-size: 24px;
    letter-spacing: -0.01em;
    padding: 0 var(--space-6) var(--space-5);
}

.admin-brand .eyebrow {
    margin-bottom: 2px;
    color: var(--accent);
}

.sidebar-menu-toggle {
    display: none;
}

.admin-sidebar nav {
    display: flex;
    flex-direction: column;
    flex: 1;
}

.admin-sidebar nav a {
    display: flex;
    align-items: center;
    gap: 12px;
    color: var(--text);
    text-decoration: none;
    font-size: var(--font-size-nav);
    font-weight: 600;
    padding: 9px var(--space-6);
    border-left: 3px solid transparent;
    transition: color var(--transition-fast), background-color var(--transition-fast), border-color var(--transition-fast);
}

.admin-sidebar nav a.active,
.admin-sidebar nav a:hover {
    color: var(--accent-hover);
    border-left-color: var(--accent);
    background: var(--accent-soft);
}

/* Deploy: the sidebar's one action link, right after SEO. It keeps a resting
   background (the same green tint an active link gets) as an inset rounded
   chip, so it stands out from the content sections above it. Hover and the
   current page use a lighter, brighter tint of that same green, never a
   darker one. */
.admin-sidebar nav a.nav-deploy {
    margin: var(--space-3) var(--space-4);
    padding: 9px var(--space-4);
    border-left-width: 0;
    border-radius: var(--radius-md);
    background: var(--accent-soft);
}

.admin-sidebar nav a.nav-deploy:hover,
.admin-sidebar nav a.nav-deploy.active {
    background: rgba(76, 183, 49, 0.24);
}

/* Categories is the one nested content group in the sidebar. Native
   details/summary keeps it keyboard-accessible without adding JavaScript. */
.nav-group {
    display: block;
}

.nav-group summary {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: 9px var(--space-6);
    border-left: 3px solid transparent;
    color: var(--text);
    font-size: var(--font-size-nav);
    font-weight: 600;
    cursor: pointer;
    list-style: none;
    transition: color var(--transition-fast), background-color var(--transition-fast), border-color var(--transition-fast);
}

.nav-group summary::-webkit-details-marker {
    display: none;
}

/* list-style: none alone doesn't reliably collapse the marker's reserved
   layout space in every engine — this is the standard (non-webkit-only)
   way to fully remove it, box included, not just the glyph. Without this,
   a NESTED .nav-group's <summary> (one living inside another group's own
   .nav-submenu) rendered its text shifted right of a plain <a> sibling at
   the same nesting depth despite both having identical padding-left — the
   marker's own box was still there, just invisible. */
.nav-group summary::marker {
    content: "";
}

.nav-group summary:hover,
.nav-group summary.active {
    color: var(--accent-hover);
    border-left-color: var(--accent);
    background: var(--accent-soft);
}

.nav-group summary.active .nav-icon {
    color: var(--text);
}

.nav-chevron {
    margin-left: auto;
    color: var(--text-faint);
    font-size: 1.2em;
    line-height: 1;
    transition: color var(--transition-fast), transform var(--transition-fast);
}

.nav-group[open] .nav-chevron {
    color: var(--accent);
    transform: rotate(90deg);
}

.nav-submenu {
    padding: var(--space-1) 0 var(--space-2);
    background: var(--accent-subtle);
}

.admin-sidebar nav .nav-submenu a {
    padding: var(--space-2) var(--space-4) var(--space-2) calc(var(--space-6) + var(--space-6) + var(--space-2));
    border-left-color: transparent;
    background: transparent;
    color: var(--text-muted);
    font-size: var(--font-size-sm);
    font-weight: 500;
}

.admin-sidebar nav .nav-submenu a:hover {
    background: transparent;
    color: var(--text);
}

.admin-sidebar nav .nav-submenu a.active {
    border-left-color: var(--accent);
    background: var(--accent-subtle-strong);
    color: var(--accent-hover);
}

/* A .nav-group nested one level deeper inside another .nav-submenu —
   same disclosure mechanics, shifted right and toned down a step so the
   hierarchy still reads at a glance. */
.nav-submenu .nav-group summary {
    padding: var(--space-2) var(--space-4) var(--space-2) calc(var(--space-6) + var(--space-6) + var(--space-2));
    /* Same 3px (transparent) left border a plain link in a submenu has, so a group's
       label starts at exactly the same x as the links around it. */
    border-left: 3px solid transparent;
    font-size: var(--font-size-sm);
    font-weight: 500;
    color: var(--text-muted);
}

.nav-submenu .nav-group summary:hover,
.nav-submenu .nav-group summary.active {
    border-left-color: transparent;
    background: transparent;
    color: var(--text);
}

/* Nested groups (Games, News, ... inside "Content") drop their icon: the
   parent group already carries one, and the extra indent would otherwise
   push a second icon column into the submenu. */
.nav-submenu .nav-group summary .nav-icon {
    display: none;
}

.nav-submenu .nav-submenu {
    /* No padding of its own: the outer submenu already spaces the rows, so an open
       group would otherwise add extra space above and below its links. */
    padding: 0;
    background: transparent;
}

.nav-submenu .nav-submenu a {
    padding-left: calc(var(--space-6) + var(--space-6) + var(--space-6) + var(--space-2));
}

/* Icon rail — each item gets its own resting color (see tokens above) so
   the sidebar reads at a glance. Hover/active state on the label doesn't
   touch it; only being the ACTIVE item turns the icon white, which is a
   stronger, more specific selector than the per-item color class below. */
.nav-icon {
    display: block;
    width: 18px;
    height: 18px;
    flex-shrink: 0;
}

.icon-dashboard { color: var(--nav-dashboard); }
.icon-content { color: var(--nav-content); }
.icon-games { color: var(--nav-games); }
.icon-news { color: var(--nav-news); }
.icon-reviews { color: var(--nav-reviews); }
.icon-guides { color: var(--nav-guides); }
.icon-opinions { color: var(--nav-opinions); }
.icon-deals { color: var(--nav-deals); }
.icon-upcoming-games { color: var(--nav-upcoming-games); }
.icon-media { color: var(--nav-media); }
.icon-categories { color: var(--nav-categories); }
.icon-submissions { color: var(--nav-submissions); }
.icon-forms { color: var(--nav-forms); }
.icon-redirects { color: var(--nav-redirects); }
.icon-seo { color: var(--nav-seo); }
.icon-integrations { color: var(--nav-integrations); }
.icon-trash { color: var(--nav-trash); }
.icon-settings { color: var(--nav-settings); }
.icon-maintenance { color: var(--nav-maintenance); }
.icon-ai-context { color: var(--nav-ai-context); }
.icon-ai-providers { color: var(--nav-ai-providers); }
.icon-import-export { color: var(--nav-import-export); }

.admin-sidebar nav a.active .nav-icon {
    color: var(--text);
}

/* Notifications: the bell, with the unread count in a red circle on its top-right corner (the badge every
   social app uses). The circle has a ring in the sidebar colour so it reads as sitting ON the icon. */
.icon-notifications { color: var(--nav-dashboard); }

.nav-icon-with-badge {
    position: relative;
    display: inline-flex;
    flex-shrink: 0;
}

.nav-badge {
    position: absolute;
    top: -7px;
    right: -9px;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: 999px;
    background: var(--danger);
    box-shadow: 0 0 0 2px var(--sidebar-bg, var(--bg));
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
    box-sizing: border-box;
}

/* Settings section header — same icon+label layout as a nav link, but
   not one itself (no href, no active/hover state); it just introduces
   the Integrations/Languages links that follow it. */
.nav-section-label {
    display: flex;
    align-items: center;
    gap: 12px;
    color: var(--text-faint);
    font-size: 13px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding: 9px var(--space-6);
    margin-top: var(--space-4);
}

.sidebar-version {
    margin-top: var(--space-5);
    padding: 0 var(--space-6);
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--text-faint);
    text-align: left;
}

.sidebar-footer {
    padding: var(--space-4) var(--space-5) 0;
    border-top: 1px solid var(--border);
}

.sidebar-footer .button {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    height: auto;
    padding: 10px 28px;
    border: none;
    border-radius: 3px;
    font-size: 16px;
    line-height: 1.4;
}

.admin-main {
    flex: 1;
    min-width: 0;
    width: 100%;
    max-width: var(--page-max-width);
    margin-inline: auto;
    padding: var(--space-8) var(--space-9) var(--space-10);
}

.page-header {
    padding-bottom: var(--space-6);
    margin-bottom: var(--space-7);
    border-bottom: 1px solid var(--border);
}

.page-header .eyebrow {
    margin-bottom: var(--space-2);
    color: var(--accent);
}

.page-header p {
    max-width: 680px;
    margin: var(--space-2) 0 0;
    color: var(--text-muted);
    font-size: 14px;
}

.section-heading {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--space-5);
    margin-bottom: var(--space-4);
}

.section-heading .eyebrow {
    margin-bottom: var(--space-1);
}

.section-heading p {
    margin: 0;
    color: var(--text-muted);
    font-size: 13px;
}

h1 {
    margin: 0;
    font-size: clamp(26px, 2.4vw, 34px);
    font-weight: 720;
    line-height: 1.15;
    letter-spacing: -0.035em;
}

h2 {
    font-size: 17px;
    font-weight: 700;
    line-height: 1.3;
    letter-spacing: -0.015em;
    margin: 0;
}

/* A quieter in-form heading — e.g. "Case Study fields" appearing partway
   down a form once a template's been assigned. */
.subsection-heading {
    font-size: 13px;
    font-weight: 600;
    color: var(--text-muted);
    border-top: 1px solid var(--border);
    padding-top: var(--space-4);
    margin: var(--space-2) 0 var(--space-3);
}

/* ==========================================================================
   Tables
   ========================================================================== */

table {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    margin-bottom: var(--space-6);
    overflow: hidden;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
}

th, td {
    text-align: left;
    padding: 12px 14px;
    border-bottom: 1px solid var(--border);
    vertical-align: middle;
}

th {
    color: var(--text-faint);
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    background: var(--surface-2);
}

tbody tr:last-child td {
    border-bottom: 0;
}

tbody tr {
    transition: background-color var(--transition-fast);
}

tbody tr:hover {
    background: rgba(255, 255, 255, 0.02);
}

/* ==========================================================================
   Forms
   ========================================================================== */

/* height, not min-height — a <select> and a <button> compute their own
   intrinsic content height slightly differently even with identical
   padding/border/font-size/line-height (native form-control replaced-
   element sizing isn't fully normalized by appearance:none across every
   engine), so leaving this as a floor still let one control's intrinsic
   height sneak past the other's. An explicit height forces the box to
   exactly --control-height regardless, matching the button rule below. textarea's
   own later min-height:110px rule still overrides this safely, since
   min-height always wins over a smaller explicit height. line-height is
   pinned to 1.35 (matching the button rule) rather than inheriting
   body's 1.55, for the same reason — no property here should be left to
   each control's own default resolution once two different elements
   need to render at the identical height next to each other. */
input:not([type]), input[type="text"], input[type="email"],
input[type="password"], input[type="number"], input[type="url"],
input[type="date"], input[type="datetime-local"], input[type="search"], textarea, select {
    height: var(--control-height);
    background: var(--bg);
    border: 1px solid var(--border);
    color: var(--text);
    padding: 8px 12px;
    border-radius: 8px;
    font-size: 14px;
    line-height: 1.35;
    width: 100%;
    font-family: inherit;
    color-scheme: dark;
    transition: border-color var(--transition-fast), background-color var(--transition-fast), box-shadow var(--transition-fast);
}

select {
    appearance: none;
    -webkit-appearance: none;
    padding-right: var(--space-6);
    background-image:
        linear-gradient(45deg, transparent 50%, var(--text-muted) 50%),
        linear-gradient(135deg, var(--text-muted) 50%, transparent 50%);
    background-position:
        calc(100% - var(--space-4)) calc(50% - var(--select-arrow-lift)),
        calc(100% - var(--space-3)) calc(50% - var(--select-arrow-lift));
    background-size:
        var(--select-arrow-size) var(--select-arrow-size),
        var(--select-arrow-size) var(--select-arrow-size);
    background-repeat: no-repeat;
}

input::placeholder,
textarea::placeholder {
    color: var(--text-faint);
}

/* Keep the platform calendar/time picker, but give it the same dark field
   chrome and readable, tabular timing as every other CMS control. */
input[type="datetime-local"] {
    font-variant-numeric: tabular-nums;
}

input:hover, textarea:hover, select:hover {
    border-color: var(--border-strong);
}

/* Border-color is this field's own focus indicator, so the global
   :focus-visible outline is redundant here — without :focus-visible,
   the two would stack into a thick double ring around every input. */
input:focus, textarea:focus, select:focus {
    border-color: var(--border-focus);
    background: var(--surface-2);
    box-shadow: var(--shadow-focus);
    outline: none;
}

textarea {
    min-height: 110px;
    resize: vertical;
}

/* Rich text editor (content body — see admin/rich-text-editor.js
   and includes/rich-text.php). One bordered wrapper carries the toolbar
   and the contenteditable area flush against each other (the border
   between them, not a margin), so as a single unit it drops into
   `.field > label + *` / `.field > * + *` exactly like the plain
   textarea it replaces — no new spacing rule needed at the .field level. */
.rte {
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg);
    overflow: hidden;
    transition: border-color var(--transition-fast), background-color var(--transition-fast), box-shadow var(--transition-fast);
}

.rte:focus-within {
    border-color: var(--border-focus);
    background: var(--surface-2);
    box-shadow: var(--shadow-focus);
}

.rte-toolbar {
    display: flex;
    gap: var(--space-1);
    padding: var(--space-2);
    border-bottom: 1px solid var(--border);
}

.rte-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    padding: 0;
    background: transparent;
    border: 1px solid transparent;
    border-radius: 6px;
    color: var(--text-muted);
}

.rte-btn svg {
    width: 16px;
    height: 16px;
}

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

.rte-btn.active {
    background: var(--accent-soft);
    border-color: var(--border-focus);
    color: var(--accent);
}

.rte-editor {
    min-height: 110px;
    padding: 9px 12px;
    font-size: 14px;
    line-height: 1.55;
    color: var(--text);
}

.rte-editor p {
    margin: 0;
}

.rte-editor p + p {
    margin-top: var(--space-3);
}

.rte-editor:empty::before {
    content: "Write a description…";
    color: var(--text-faint);
}

label {
    display: block;
    margin-bottom: 6px;
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: #C7C7D1;
}

.field { margin-bottom: var(--space-5); }

/* A form-native-value combobox for curated catalogue selectors. The visible
   input is both the search field and the current selection; its menu stays
   attached to that control rather than making editors filter a separate
   dropdown. The hidden input is the ordinary form value submitted to PHP. */
.searchable-select {
    position: relative;
}

.searchable-select__control {
    position: relative;
}

.searchable-select__input {
    padding-right: calc(var(--control-height) * 2 + var(--space-2));
}

.searchable-select__actions {
    position: absolute;
    inset: 0 var(--space-1) 0 auto;
    display: flex;
    align-items: center;
}

.searchable-select .searchable-select__icon {
    width: calc(var(--control-height) - var(--space-2));
    height: calc(var(--control-height) - var(--space-2));
    margin: 0;
    padding: var(--space-2);
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-muted);
    transition: all var(--transition-fast);
}

.searchable-select .searchable-select__icon:hover {
    background: var(--surface-3);
    color: var(--text);
}

.searchable-select__icon svg {
    width: 18px;
    height: 18px;
    transition: all var(--transition-fast);
}

.searchable-select__icon[aria-expanded="true"] svg {
    transform: rotate(180deg);
}

.searchable-select__icon[hidden],
[data-pick-edit][hidden] {
    display: none;
}

.searchable-select:focus-within .searchable-select__input {
    border-color: var(--border-focus);
    box-shadow: var(--shadow-focus);
}

.searchable-select__list {
    position: absolute;
    top: calc(100% + var(--space-1));
    z-index: 20;
    width: 100%;
    max-height: min(300px, 45vh);
    overflow-y: auto;
    overscroll-behavior: contain;
    scroll-padding: var(--space-1);
    padding: var(--space-1);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--surface-2);
    box-shadow: var(--shadow-card);
}

.searchable-select__list[hidden] {
    display: none;
}

.searchable-select__option {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    width: 100%;
    height: auto;
    min-height: 38px;
    margin-left: 0;
    padding: var(--space-2) var(--space-3);
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text);
    font: inherit;
    line-height: 1.35;
    text-align: left;
    white-space: normal;
    cursor: pointer;
    transition: all var(--transition-fast);
}

.searchable-select__option:hover,
.searchable-select__option.is-active {
    background: var(--accent-soft);
    color: var(--accent-hover);
}

/* Do not depend on the browser's UA [hidden] rule here: buttons have a
   project-wide display declaration, so filtering needs an explicit state
   that wins regardless of browser or stylesheet order. */
.searchable-select__option.is-filtered-out {
    display: none;
}

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

.searchable-select__option-label {
    min-width: 0;
    overflow-wrap: anywhere;
}

.searchable-select__option-meta {
    flex-shrink: 0;
    color: var(--text-muted);
    font-size: 12px;
    font-weight: 400;
}

.searchable-select__option[aria-selected="true"] {
    color: var(--accent-hover);
    background: var(--accent-soft);
    font-weight: 600;
}

.searchable-select__empty {
    display: none;
    margin: var(--space-2) var(--space-3);
    color: var(--text-muted);
    font-size: 13px;
}

.searchable-select__empty.is-visible {
    display: block;
}

/* Multi-game picker (render_game_multi_picker() in includes/layout.php, behaviour in
   admin/game-picker.js). Chosen games are chips above a search box; the search box
   reuses the .searchable-select visuals. Order matters: the first chip leads. */
.game-chip-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin: 0 0 var(--space-3);
    padding: 0;
    list-style: none;
}

.game-chip-list:empty {
    display: none;
}

.game-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    max-width: 100%;
    padding: var(--space-1) var(--space-2) var(--space-1) var(--space-3);
    border: 1px solid var(--border-strong);
    border-radius: 999px;
    background: var(--surface-2);
    color: var(--text);
    font-size: 13px;
    font-weight: 600;
    transition: all var(--transition-fast);
}

.game-chip.is-lead {
    border-color: var(--accent);
    background: var(--accent-soft);
}

.game-chip__label {
    min-width: 0;
    overflow-wrap: anywhere;
}

.game-chip__badge {
    padding: var(--space-1) var(--space-2);
    border-radius: 999px;
    background: var(--accent);
    color: var(--bg);
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.game-chip .game-chip__action {
    height: auto;
    margin: 0;
    padding: 0 var(--space-1);
    border: 0;
    background: none;
    color: var(--text-muted);
    font: inherit;
    font-size: 12px;
    font-weight: 400;
    text-decoration: underline;
    cursor: pointer;
    transition: all var(--transition-fast);
}

.game-chip .game-chip__action:hover {
    background: none;
    color: var(--accent-hover);
}

.game-chip .game-chip__remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
    transition: all var(--transition-fast);
}

.game-chip .game-chip__remove:hover {
    background: var(--surface-3);
    color: var(--text);
}

.game-chip__remove svg {
    width: 14px;
    height: 14px;
}

.game-chip [hidden] {
    display: none;
}

.game-picker__summary {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    margin: 0 0 var(--space-3);
}

.game-picker__count {
    color: var(--text-muted);
    font-size: 13px;
}

.game-picker__empty {
    margin-bottom: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-md);
    color: var(--text-muted);
    font-size: 13px;
}

.game-picker__empty[hidden] {
    display: none;
}

/* Shared collection-list actions. Kept above the table so selecting rows
   never makes the table jump horizontally or pushes per-row actions around. */
.bulk-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-3);
    min-height: var(--control-height);
    margin: 0 0 var(--space-4);
}

.bulk-actions[hidden] {
    display: none;
}

.bulk-actions__count {
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 13px;
}

.bulk-select-all {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0 0 var(--space-3);
    color: var(--text-muted);
    cursor: pointer;
}

.bulk-select-all input {
    width: auto;
    margin: 0;
}

/* A form stack makes the gap between field groups explicit instead of
   relying on each individual field to leave space below itself. It is useful
   for cards with an introductory hint, where the first control must never
   visually attach to the explanatory copy. */
.form-stack {
    display: grid;
    gap: var(--space-5);
    max-width: 860px;
}

.form-stack > .field,
.form-stack > .field-row {
    margin-bottom: 0;
}

.form-toggle-group {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-3);
}

.form-toggle {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-height: 48px;
    margin: 0;
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface-2);
    color: var(--text);
    cursor: pointer;
    transition: all var(--transition-fast);
}

.form-toggle:hover {
    border-color: var(--border-strong);
    background: var(--surface-elevated);
}

.form-toggle:focus-within {
    border-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent-soft);
}

.form-toggle input {
    width: auto;
    margin: 0;
    flex: 0 0 auto;
}

/* A row of fields side by side (e.g. Key / Label / Type on a custom field
   editor) — behaves like a single .field from the outside (same
   margin-bottom to whatever comes after it), but lays its own .field
   children out horizontally instead of stacking them. */
.field-row {
    /* A subgrid row: every field spans three shared rows (its label, its control, then
       a hint), so labels line up with each other and controls line up with each
       other. A one-line label sits right on its control, and a label that wraps or
       carries a unit only pushes the controls down in THIS row, not by a fixed
       reserved gap. */
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    grid-template-rows: auto auto auto;
    column-gap: var(--space-4);
    align-items: start;
    margin-bottom: var(--space-5);
}

.field-row > .field {
    display: grid;
    grid-template-rows: subgrid;
    grid-row: span 3;
    row-gap: 0;
}

/* A field with more than label + control + one hint cannot be placed in the shared
   rows without overlapping; it falls back to normal flow (rows still line up at the
   top, only the controls may differ in height). */
.field-row > .field:has(> :nth-child(4)) {
    display: block;
}

/* A field that is only checkbox rows has no label/control pair: it sits across the
   rows, at the bottom, level with the controls beside it. */
.field-row > .field:has(> .checklist-item, > .form-toggle, > .switch) {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    gap: var(--space-2);
}

input[type="checkbox"],
input[type="radio"] {
    accent-color: var(--accent);
}

/* Native file inputs — a bare <input type="file"> renders as an unstyled
   OS/browser "Choose File" button by default, which breaks the design
   system every time one gets added (this has happened repeatedly, from
   multiple different admin pages and AI-authored diffs alike). Rather
   than relying on every future field to remember the hidden-input-plus-
   custom-trigger-button pattern (see render_featured_image_field() in
   includes/layout.php for that pattern — still the right call when a
   field needs a live preview/remove control alongside it), style the
   native control itself so ANY <input type="file"> — present or future,
   anywhere in the admin — always matches the rest of the UI with zero
   extra markup and no class required.

   The outer <input> itself gets NO box of its own (no border/background/
   padding) — only the ::file-selector-button portion below looks like a
   button. An earlier version put a bordered, backgrounded box on the
   outer input too, which wrapped the already-boxed file-selector-button
   in a second, larger box — a visible "card around the button" that
   didn't belong there. Layout/text styling only here; visual chrome
   lives on the button pseudo-element exclusively. */
input[type="file"] {
    display: inline-flex;
    align-items: center;
    width: auto;
    color: var(--text-muted);
    font-size: 13px;
    font-family: inherit;
}

input[type="file"]:focus-visible {
    outline: 2px solid var(--border-focus);
    outline-offset: 2px;
}

/* The actual clickable button portion, styled to match .button.secondary
   — a neutral "choose/open" action, distinct from the primary Save/
   Submit button that normally sits right next to it in an .upload-row. */
input[type="file"]::file-selector-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: rgba(74, 143, 209, 0.1);
    border: 1px solid rgba(74, 143, 209, 0.45);
    color: #8FC5F7;
    padding: 7px 12px;
    margin-right: var(--space-3);
    border-radius: var(--btn-radius);
    font-size: 13px;
    font-weight: var(--btn-font-weight);
    font-family: inherit;
    cursor: pointer;
    transition: background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}

input[type="file"]::file-selector-button:hover {
    background: var(--btn-secondary-bg-hover);
    border-color: var(--btn-secondary-bg-hover);
    color: var(--btn-secondary-text-hover);
}

/* The label-wrapped-hidden-input pattern (gallery replace/add buttons,
   render_featured_image_field()) relies on the browser's native
   [hidden] { display: none }. The generic input[type="file"] rule above
   has higher specificity than that UA rule and was winning, popping the
   "hidden" file input back into view as a native control floating next
   to its custom trigger button. */
input[type="file"][hidden] {
    display: none;
}

.field-row .field {
    flex: 1;
    min-width: 0;
    margin-bottom: 0;
}

/* Labels in a horizontal row. Controls stay level with each other because the row
   is a subgrid (see .field-row above), so a label no longer needs a reserved
   two-line area: adding a unit, helper word or a longer translated label moves the
   controls in its own row together, never one lower than its peers. A label that IS
   the control (a checkbox row) is excluded: it keeps its own horizontal layout. */
.field-row .field > label:where(:not(.checklist-item, .form-toggle, .switch)) {
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    margin-bottom: var(--space-2);
}

/* Kept for markup that opts in explicitly; the behaviour is now the default above. */
.field-row--aligned-labels .field > label:where(:not(.checklist-item, .form-toggle, .switch)) {
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    margin-bottom: var(--space-2);
}

/* Search box + type filter sitting above a .page-picker <select> — no
   margin-top rule needed here despite living inside a .field: it's a
   second child of .field, so the standard .field > * + * spacing already
   applies automatically (see the spacing-rule comment further down). */
.page-picker-toolbar {
    display: flex;
    gap: var(--space-3);
}

.page-picker-toolbar .page-picker-search {
    flex: 2;
    min-width: 0;
}

.page-picker-toolbar .page-picker-type-filter {
    flex: 1;
    min-width: 0;
}

/* Structural fix, not a per-element one: ANY second-or-later child inside
   a .field gets consistent spacing from the thing above it, automatically
   — a new hint, preview image, or helper row added later doesn't need its
   own margin-top rule remembered and written by hand. The label→control
   gap is the one deliberate exception (stays tight, 4px) since a label
   should read as attached to its control, not as a separate block. If you
   add a new kind of content inside .field, you should NOT need to touch
   this section at all.
   Specificity note: ".field > label + *" (class+type+universal) is more
   specific than ".field > * + *" (class+universal+universal), so it wins
   regardless of source order — no !important needed. */
.field > * + * {
    margin-top: var(--space-3);
}

/* "label + control" is only for a label that sits ABOVE its control. A label that
   IS the control (a checkbox row, a switch, a toggle) is a normal block: whatever
   follows it, such as its hint, gets the usual space from .field > * + * above.
   Matching every label here used to glue the hint to the checkbox row. */
.field > label:where(:not(.checklist-item, .form-toggle, .switch)) + * {
    /* Not "4px to match the label" — zero. label's own margin-bottom (see
       the label rule above) already IS that 4px gap. Giving the control a
       margin-top here too would double it: text inputs/selects/textareas
       are inline-level by default and don't collapse margins with a
       preceding block sibling the way two <div>s would, so 4px + 4px
       would render as a full 8px instead of the intended 4px. Margin-top
       is the only spacing tool in this file on purpose — nothing in
       .field should ever declare its own margin-bottom, so there's never
       a second value to collapse or double up with. */
    margin-top: 0;
}

/* Checkbox group for assigning categories — a category can apply to many
   items, so this is a checklist, not a single select. */
.checklist {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-4);
    padding: var(--space-3);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    /* No margin-bottom here on purpose — see the .field > * + * comment;
       spacing after this comes from the next element's margin-top. */
}

.checklist-item {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 14px;
    font-weight: 400;
    letter-spacing: normal;
    text-transform: none;
    color: var(--text);
    margin-bottom: 0;
    cursor: pointer;
}

.checklist-item input[type="checkbox"] {
    width: auto;
}

/* Several checkbox rows stacked in one field: close together, but not touching. */
.field > .checklist-item + .checklist-item {
    margin-top: var(--space-2);
}

/* Instant on/off control for table rows (Tracking & Analytics integrations)
   — distinct from a plain checkbox: submits its own out-of-band form
   immediately on change (see admin/integrations.php's "toggle" action)
   instead of waiting for a page-level Save button, so flipping it reads as
   "flip and done." The real checkbox is visually hidden but stays in the
   tab order and keyboard/screen-reader operable; the ".switch" label next
   to it is the whole visible control. */
.switch-input {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
}

.switch {
    display: inline-block;
    position: relative;
    width: 36px;
    height: 20px;
    flex: none;
    background: var(--surface-3);
    border: 1px solid var(--border);
    border-radius: 999px;
    cursor: pointer;
    transition: background-color var(--transition-fast), border-color var(--transition-fast);
}

.switch::before {
    content: "";
    position: absolute;
    top: 1px;
    left: 1px;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--text-faint);
    transition: transform var(--transition-fast), background-color var(--transition-fast);
}

.switch-input:hover + .switch {
    border-color: var(--border-strong);
}

.switch-input:hover + .switch::before {
    background: var(--text-muted);
}

.switch-input:checked + .switch {
    background: var(--accent-soft);
    border-color: var(--accent);
}

.switch-input:checked + .switch::before {
    transform: translateX(16px);
    background: var(--accent);
}

.switch-input:checked:hover + .switch::before {
    background: var(--accent-hover);
}

.switch-input:focus-visible + .switch {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* ==========================================================================
   Buttons
   ========================================================================== */

/* height (not min-height) matches the --control-height form-field height (inputs/
   selects, see the Forms section above) exactly — a floor alone still
   left room for this control's own intrinsic content sizing to render
   a pixel or two off from a field's, which showed up the moment the two
   sat aligned by their bottom edges in a flex row (e.g. a search field
   plus its Search button, or a Status/Category/Sort row plus a Filter
   button). An explicit height removes that remaining slack entirely. */
button, .button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    height: var(--control-height);
    background: var(--btn-primary-bg);
    color: var(--btn-primary-text);
    border: 1px solid transparent;
    padding: var(--btn-padding-y) var(--btn-padding-x);
    border-radius: var(--btn-radius);
    font-size: var(--btn-font-size);
    font-weight: var(--btn-font-weight);
    font-family: inherit;
    line-height: 1.35;
    cursor: pointer;
    text-decoration: none;
    white-space: nowrap;
    transition: background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast), transform var(--transition-fast);
}

button:hover, .button:hover {
    background: var(--btn-primary-bg-hover);
    color: var(--btn-primary-text-hover);
}

button:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

button:active, .button:active {
    transform: translateY(1px);
}

button.danger, .button.danger {
    background: var(--danger-soft);
    border-color: rgba(214, 54, 56, 0.45);
    color: #FF8F91;
}

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

button.secondary, .button.secondary {
    background: rgba(74, 143, 209, 0.1);
    border-color: rgba(74, 143, 209, 0.45);
    color: #8FC5F7;
}

button.secondary:hover, .button.secondary:hover {
    background: var(--btn-secondary-bg-hover);
    color: var(--btn-secondary-text-hover);
    border-color: var(--btn-secondary-bg-hover);
}

button.duplicate, .button.duplicate {
    background: rgba(106, 84, 191, 0.12);
    border-color: rgba(106, 84, 191, 0.45);
    color: #B7A8F3;
}

button.duplicate:hover, .button.duplicate:hover {
    background: var(--btn-duplicate-bg-hover);
    color: var(--btn-duplicate-text-hover);
    border-color: var(--btn-duplicate-bg-hover);
}

/* Consistent spacing wherever two buttons sit next to each other,
   regardless of element type or which variant. */
button + button,
button + .button,
.button + button,
.button + .button {
    margin-left: var(--btn-gap);
}

/* Purpose-built action layouts own their spacing through gap. Neutralize the
   legacy sibling margin there so a group never gets both gap and margin. */
:is(.form-actions, .button-group, .toolbar-form, .table-actions, .source-card__actions) > :is(button, .button) + :is(button, .button) {
    margin-left: 0;
}

/* Buttons repeat multiple times per row in a table (Edit/Save/Delete on
   every line), so they use a size a step down from the form-scale one
   above — full-size buttons don't comfortably fit three-to-a-row. This is
   scoped to any button living inside a <td>, so it applies automatically
   with no extra classes needed in markup. min-height is intentionally
   NOT reduced here — a table row's Status <select> stays at the full
   --control-height field height (see the Forms section), so a shorter button next
   to it would reintroduce the exact same field/button height mismatch
   this file just fixed for toolbars. Only padding-x/font-size step down,
   keeping the row visually denser without misaligning the button against
   whatever field sits beside it. */
td button, td .button {
    padding: 7px 12px;
    font-size: 13px;
}

/* Actions always live in the last column in this app's tables — that
   column never wraps its buttons onto a second line. width: 1% is the
   standard "shrink this column to its content" trick for an auto-layout
   table: paired with nowrap it stops this column (and .mono cells below)
   from ever being allocated more width than their content needs, so a
   table's one open-ended text column (usually Title) gets whatever's
   left instead of being squeezed by an actions cell that grew a button. */
td:last-child {
    white-space: nowrap;
    width: 1%;
}

/* Compact horizontal action rail for tables with more than one form per
   row (for example integrations.php's revoke/delete controls). */
.table-actions {
    display: flex;
    align-items: center;
    gap: var(--btn-gap);
}

.table-actions form {
    margin: 0;
}

/* Slug/count/date cells are short, fixed-shape content — same shrink-to-fit
   reasoning as td:last-child above, so they never compete with Title for
   space either. Same for a Status column, whether it's a quick-edit
   <select> (News, Reviews, Guides, Opinions) or a read-only .status-badge
   (everywhere else) — both are short, fixed-shape content too. */
td.mono,
td:has(select),
td:has(.status-badge) {
    white-space: nowrap;
    width: 1%;
}

/* width: 1% above shrinks the CELL to content width, but a <select>'s own
   intrinsic width isn't computed from its option text the way a text node
   is — left alone it can collapse to a sliver that only shows the
   dropdown arrow, clipping "Published"/"Draft" entirely. This floor keeps
   it legible regardless of what the surrounding table layout decides. */
td select {
    min-width: 110px;
}

/* Column roles for a <table>. Browsers share a table's width by content, so without a hint a long text column and a short one
   fight and the short one is squeezed (the Decision Log's story column was a sliver beside a 330px dropdown). Give the ONE
   open-ended column .col-fill; short columns keep their natural width, or a floor with .col-min-md, or never wrap with
   .col-compact. (th and td selectors, so they beat the `td:last-child` shrink rule above.) */
th.col-fill, td.col-fill {
    width: 100%;
    min-width: 14rem;
    white-space: normal;
}
th.col-compact, td.col-compact {
    white-space: nowrap;
}
th.col-min-sm, td.col-min-sm {
    min-width: 9rem;
}
th.col-min-md, td.col-min-md {
    min-width: 7.5rem;
}
/* A last column holding free text (game names) may wrap instead of forcing the table wider than its card. */
th.col-wrap, td.col-wrap {
    white-space: normal;
    width: auto;
    min-width: 10rem;
    max-width: 14rem;
}
/* A short second line under a cell's main value (a review state under a decision badge). */
.cell-note {
    display: block;
    white-space: normal;   /* a status cell is nowrap; its note may wrap */
    margin-top: var(--space-1);
    font-size: 12px;
    color: var(--text-muted);
}
th.col-min-lg, td.col-min-lg {
    min-width: 13rem;
}
/* A short label column (two lines for a long label) beside editable fields. */
th.col-label, td.col-label {
    min-width: 11rem;
}

/* A cell holding a text field (Redirects' old/new path) is an editing column,
   not a short data value, so the shrink-to-fit rule above must not apply even
   when the cell is .mono: a width: 100% field has no content width of its own,
   and the column collapsed to a few pixels. It takes its share of the table
   instead, with a floor so the path stays readable. */
td:has(> input:is([type="text"], [type="url"], [type="email"], [type="search"], :not([type]))) {
    width: auto;
    white-space: normal;
}

td > input:is([type="text"], [type="url"], [type="email"], [type="search"], :not([type])) {
    min-width: var(--table-field-min-width);
}

.item-title {
    font-weight: 700;
    color: white;
}

/* ==========================================================================
   List rows — unified content-list layout

   Every content list that shows entity rows with row actions (Portfolio,
   Pages/Blog posts, Categories, Templates, Trash, Forms) shares this one
   markup/CSS instead of each table hand-rolling its own <table> column
   widths — same handful of classes everywhere, per the Figma row spec
   (file 6E6K4GlKRMlWl8AOT1j1U5, node 1:4784). One shared header row up
   top (labels once, .eyebrow style), then each entry is a plain value
   row followed by its own actions-pill row underneath — a div-based
   table rather than a real <table>, which is what lets the actions pill
   sit indented under just that one row instead of spanning every column.

   Every value here maps onto a token or class that already existed before
   this component did — .eyebrow for header labels, .mono for slugs,
   .duplicate/.danger for action-color identity, --border-strong for the
   actions pill background (#34343F, effectively Figma's #333), the
   default button/.button green for Save — confirming the Figma redesign
   was built from this app's own tokens rather than introducing a
   parallel, competing set.
   ========================================================================== */

.list-table {
    display: flex;
    flex-direction: column;
    margin-bottom: var(--space-6);
    /* Rounded corners clip the rows, but a table wider than its card must scroll,
       never silently cut off its last column (that hid delete buttons). */
    overflow-x: auto;
    overflow-y: hidden;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
}

.list-header {
    display: flex;
    align-items: center;
    gap: var(--space-5);
    padding: 11px var(--space-5);
    border-bottom: 1px solid var(--border);
    margin-bottom: 0;
    background: var(--surface-2);
}

.list-header .eyebrow {
    margin-bottom: 0;
}

.list-row {
    padding: var(--space-4) var(--space-5);
    /* Lets the news inbox's "Reject" button (admin/news-inbox.php) collapse a
       row out via JS-toggled inline styles instead of a full page reload,
       without introducing a new transition token. */
    transition: opacity var(--transition-fast), transform var(--transition-fast),
        max-height var(--transition-fast), padding-top var(--transition-fast),
        padding-bottom var(--transition-fast), border-color var(--transition-fast),
        background-color var(--transition-fast);
}

.list-row + .list-row {
    border-top: 1px solid var(--border);
}

.list-row-main {
    display: flex;
    align-items: center;
    gap: var(--space-5);
    flex-wrap: nowrap;
    margin-bottom: var(--space-2);
}

.list-cell {
    min-width: 0;
    /* A long identifier (a table name, an e-mail) wraps inside its column instead of
       running into the next one. */
    overflow-wrap: anywhere;
}

.list-value {
    font-size: 13px;
    color: var(--text);
    line-height: 1.4;
}

.list-cell select,
.list-cell input {
    width: 100%;
}

/* Categories' inline name field: reads as plain .list-value text until
   "Edit" is clicked (categories.js strips readonly) — without this it'd
   still look like a live input box even though it can't be typed into,
   which is the exact confusion this pattern exists to avoid. */
.list-cell input[readonly] {
    background: transparent;
    border-color: transparent;
    padding: 0;
    min-height: 0;
    cursor: default;
}

/* Width steps shared across every converted list, instead of each table
   hand-tuning nth-child widths — which breaks the moment a column
   becomes conditional (this is exactly what happened to Categories on
   the Pages/Blog table before this component existed). Header cells and
   row cells share the same classes, which is what keeps columns aligned
   without either one hand-copying the other's widths.

   .admin-main caps out at 1200px (1152px inside its own padding) — the
   widest row (Portfolio: thumb/title/slug/categories/template/status/
   sort/save) adds up well within that with nowrap, so a row never breaks
   onto a second visual line. Every fixed step gets flex-shrink: 0 so
   pressure (a long category list, a narrow window) compresses .grow
   first, never these — .grow's own min-width is what finally gives if
   space really runs out. */
.w-thumb { width: 36px; flex-shrink: 0; }
.list-cell.w-check { width: 32px; flex-shrink: 0; text-align: center; }
.list-cell.grow { flex: 1 1 200px; min-width: 120px; }
.list-cell.w-slug { width: 160px; flex-shrink: 0; }
.list-cell.w-sm { width: 70px; flex-shrink: 0; }
.list-cell.w-md { width: 110px; flex-shrink: 0; }
.list-cell.w-lg { width: 140px; flex-shrink: 0; }
.list-cell.w-xl { width: 190px; flex-shrink: 0; }   /* a table name, an e-mail address, a cron schedule */
.list-cell.w-status { width: 130px; flex-shrink: 0; }
.list-cell.w-timing { width: 230px; flex-shrink: 0; }
.list-cell.w-candidate-status { width: 145px; flex-shrink: 0; }
.list-cell.w-row-actions { width: 152px; flex-shrink: 0; }
.list-cell.w-featured { width: 80px; flex-shrink: 0; text-align: center; }
.list-cell.w-btn { width: 90px; flex-shrink: 0; }
/* The inbox's publish cell: an optional "Why?" dropdown above the Publish button. Wide enough for the dropdown (it was clipped
   at the 90px of a plain button cell) and stacked with a gap, so the two never touch. */
.list-cell.w-publish {
    width: 170px;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-2);
}
.list-cell.w-publish select { min-width: 0; }
/* The same cell on a story that is already decided: its state badges ("Published", "AI") sit side by side instead of a dropdown over a button, and the
   column keeps the width of the undecided rows so the list stays aligned. */
.list-cell.w-publish--state {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: flex-start;
    align-content: flex-start;
}

.list-row-actions {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    padding: 0;
    background: transparent;
    border-radius: 0;
}

/* Resets every property the generic button/.button rule sets — a plain
   text action, not a pill button, sharing a background with its siblings
   instead of carrying its own. A single class beats the base rule's bare
   element selector (button, .button) on specificity, but Duplicate/Delete
   are also *.duplicate/*.danger buttons elsewhere in this app — button.
   duplicate and button.danger are an element+class pair, which outranks
   this single-class reset for exactly those two. Repeating the reset
   under .list-action.duplicate/.list-action.danger (two classes) is what
   wins there too — text only, colored text, never a filled pill, in every
   case. */
.list-action {
    display: inline;
    background: none;
    border: none;
    padding: 0;
    border-radius: 0;
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    line-height: normal;
    color: var(--text);
    cursor: pointer;
    text-decoration: none;
    white-space: nowrap;
    transition: color var(--transition-fast);
}

.list-action.duplicate,
.list-action.danger {
    background: none;
    padding: 0;
    border-radius: 0;
}

.list-action.duplicate {
    color: var(--text);
}

/* Default (Edit) hover shifts to the brand accent, same direction as
   every other link in the admin — .duplicate/.danger keep their own
   button counterpart's hover color instead, so each action keeps its
   identity rather than all three converging on green. Always a lighter
   shade than the resting color, never darker. */
.list-action:hover {
    background: none;
    color: var(--accent-hover);
}

.list-action.duplicate:hover {
    background: none;
    color: var(--btn-duplicate-bg-hover);
}

.list-action.danger {
    color: var(--danger);
}

.list-action.danger:hover {
    background: none;
    color: var(--danger-hover);
}

.list-action-divider {
    width: 1px;
    height: 14px;
    background: var(--border-strong);
}

/* "Create Gamer Deer article" split choice (admin/news-inbox.php): clicking
   the trigger reveals Empty draft / AI draft instead of the button
   submitting anything itself. */
.list-action-choice {
    position: relative;
    display: inline-flex;
}

/* position: fixed, not absolute — .list-table (this menu's real ancestor)
   has overflow: hidden so row backgrounds respect its rounded corners,
   which would otherwise clip this menu the moment it needs to extend past
   the table's own box (any row but the very top one). Fixed positioning
   escapes that entirely; its top/left are computed in JS from the
   trigger's own position right before it's shown, since fixed uses
   viewport coordinates, not a position relative to .list-action-choice. */
.list-action-choice-menu {
    position: fixed;
    z-index: 20;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 150px;
    padding: var(--space-2);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-card);
}

/* Every item fills the menu's width and starts at the same left edge. The global
   "button + button" sibling margin would otherwise indent every item after the
   first, so the menu owns its spacing through gap and removes that margin. */
.list-action-choice-menu .list-action {
    display: block;
    width: 100%;
    margin-left: 0;
    padding: var(--space-2);
    text-align: left;
}

/* Author CSS (the display: flex above) beats the browser's own UA
   stylesheet regardless of selector order, so [hidden] alone wouldn't
   actually hide this without an explicit override here — same gotcha
   as .modal[hidden] above. Without this, every row's menu renders open
   at once instead of just the one clicked. */
.list-action-choice-menu[hidden] {
    display: none;
}

/* Pager: render_pagination() in includes/layout.php, or AdminLists.renderPager()
   in admin/lists.js for a list paged in the browser. Same markup from both. */
.pagination {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    margin-top: var(--space-5);
}

.pagination-pages {
    display: flex;
    align-items: center;
    gap: var(--space-1);
}

.pagination-page,
.pagination-current {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--pager-size);
    height: var(--pager-size);
    padding: 0 var(--space-2);
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    font-size: 13px;
}

.pagination-page {
    border: 1px solid var(--border);
    color: var(--text);
}

.pagination-page:hover {
    border-color: var(--border-strong);
    color: var(--accent-hover);
}

.pagination-current {
    background: var(--accent);
    color: white;
}

.pagination-ellipsis {
    color: var(--text-faint);
    padding: 0 var(--space-1);
}

/* "Page [n] of N  Go": jump straight to a page. Sits after Next, set off by
   a divider so it reads as its own control rather than one more page. */
.pagination-jump {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0;
    padding-left: var(--space-3);
    border-left: 1px solid var(--border);
    color: var(--text-muted);
    font-size: var(--font-size-sm);
}

.pagination-jump label {
    margin: 0;
    font-size: var(--font-size-sm);
    color: var(--text-muted);
}

.pagination-jump input[type="number"] {
    width: var(--pager-jump-width);
    min-height: var(--pager-size);
    height: var(--pager-size);
    padding: 0 var(--space-2);
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    text-align: center;
    -moz-appearance: textfield;
    appearance: textfield;
}

.pagination-jump input[type="number"]::-webkit-inner-spin-button,
.pagination-jump input[type="number"]::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

.pagination-jump-total {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.pagination-jump button {
    min-height: var(--pager-size);
    padding: 0 var(--space-3);
}

/* Search box for a list: render_list_search() (server, in the page toolbar)
   or the instant filter admin/lists.js adds to every other list. One input
   width everywhere, so every list's search reads as the same control. */
.list-search {
    flex-wrap: nowrap;
}

.list-search input[type="search"] {
    flex: 0 1 var(--list-search-width);
    width: var(--list-search-width);
    min-width: 0;
}

.list-search-count {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    font-size: var(--font-size-sm);
    color: var(--text-muted);
    white-space: nowrap;
}

/* An instant filter that is not in the page toolbar gets its own row above
   its list, right-aligned like the toolbar's search. The row owns its
   trailing space, whatever the list's wrapper turns out to be. */
.list-filter-bar {
    display: flex;
    justify-content: flex-end;
    margin-bottom: var(--space-3);
}

.list-filter-empty {
    margin-bottom: var(--space-4);
}

:is(tr, .list-row, table, .list-table, .table-scroll).is-filtered-out {
    display: none;
}

/* Wraps admin/games.php's pagination nav + "Per page" form on one row —
   flex-start (not space-between) keeps the per-page control right after
   Next rather than pushed to the far edge. .pagination's own margin-top
   is zeroed here since this wrapper already carries it, and would
   otherwise double up whenever the nav actually renders (it no-ops on a
   single page, per render_pagination(), so the wrapper's margin is the
   only one that always applies). */
.games-list-footer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-6);
    margin-top: var(--space-5);
}

.games-list-footer .pagination {
    margin-top: 0;
}

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

/* Overrides the global input/select width:100% rule — full-width made
   sense stacked in a form field, not sitting inline next to a 5-word
   label in a toolbar row. */
.games-per-page select {
    width: auto;
    min-width: 70px;
}

/* One-line toolbar: item count on the left, the page's primary action
   (Add new) on the right — replaces the old stack of a standalone "Add
   new" button followed by its own "Heading (count)" <h2>, which repeated
   the page's own <h1> title and read as two separate blocks instead of
   one clear header. */
.page-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-3) 0;
    margin-bottom: var(--space-4);
    border-bottom: 1px solid var(--border);
}

.page-toolbar-count {
    color: var(--text-muted);
    font-size: 13px;
    margin: 0;
}

/* Header/footer location switcher on the Menus page — plain secondary
   buttons except the active one, same pattern as button variants
   elsewhere rather than a bespoke tab component. */
.tab-links {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-bottom: var(--space-4);
}

/* Filter tabs: a wrapping row of compact pills for switching between categories, each
   with its item count (the Trash page). Unlike .tab-links (full-size buttons that never
   wrap), many of them fit on any width, and a category with nothing in it is dimmed. */
.filter-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-bottom: var(--space-4);
}

.filter-tab {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    height: 34px;
    padding: 0 var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-2);
    color: var(--text-muted);
    font-size: 13px;
    font-weight: 500;
    white-space: nowrap;
    transition: background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}

.filter-tab:hover {
    border-color: var(--border-strong);
    background: var(--surface-3);
    color: var(--text);
}

.filter-tab-count {
    min-width: 22px;
    padding: 1px 6px;
    border-radius: 999px;
    background: var(--surface-3);
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 11px;
    line-height: 1.5;
    text-align: center;
    transition: background-color var(--transition-fast), color var(--transition-fast);
}

.filter-tab:hover .filter-tab-count {
    background: var(--border-strong);
    color: var(--text);
}

.filter-tab.is-empty {
    color: var(--text-faint);
}

.filter-tab.is-active {
    border-color: var(--accent);
    background: var(--accent-soft);
    color: var(--text);
}

.filter-tab.is-active:hover {
    border-color: var(--accent-hover);
}

.filter-tab.is-active .filter-tab-count {
    background: var(--accent);
    color: var(--text);
}

/* Long unbroken text (a URL, a path, a table name, a pasted script tag) wraps inside
   its own box instead of spilling over its neighbour or off the page. Zero
   specificity: any component that wants different wrapping just says so. */
:where(.admin-main) :where(p, li, dd, dt, td, th, summary, label, .hint, h1, h2, h3, h4) {
    overflow-wrap: anywhere;
}

/* ...but NOT in a table cell. `anywhere` lets a browser shrink a column to a single letter before it takes width from a
   long one (a short "Who" column became "rea / der", "(strong)" became "stron / g"). `break-word` only breaks a word when
   it cannot fit the column at its full width, so short columns keep the width of their longest word and the one open-ended
   text column (a title, a detail) gives way instead. */
:where(.admin-main) :where(td, th) {
    overflow-wrap: break-word;
}

:where(.admin-main) :where(pre) {
    max-width: 100%;
    overflow-x: auto;
}

/* ==========================================================================
   Vertical rhythm: the structural guarantee that blocks never touch.

   WHY THIS EXISTS. Spacing used to be "each component remembers its own
   margin", so every time a block followed one that had none (a hint above a
   table, a notice under a header, an action row above a card) the two sat flush
   against each other, and each case had to be found by eye and patched one by
   one. Now the CONTAINER owns the space between its children, once, here.

   THE RULE. Inside the page body, a card, a plain form, a <details> or any
   element marked .flow, a block that has a block before it gets a top margin of
   --space-3 (--space-2 right after a heading). You do not write a margin for
   this; adding a new component after an existing one just works.

   HOW IT STAYS SAFE.
   - Zero specificity (:where), so a component that sets its own margin-top
     (.hint, .form-actions, .card...) always wins, and vertical margins between
     block siblings collapse to the larger value: nothing is ever doubled.
   - Only block-level element types are targeted, never inline controls
     (buttons, inputs, labels, spans), so rows of controls are not pushed apart.
   - Containers that lay their children out themselves (.stack, .form-grid, forms
     that carry a class such as .toolbar-form, nav menus outside the page body)
     are left alone: they own their spacing through gap.
   - A hidden input, [hidden] element, script or template before a block does not
     count as "something before it", so the first visible block never gets a
     stray top margin from the CSRF field at the top of a form.

   WRITING A NEW BLOCK COMPONENT. Give it a margin-bottom if it needs trailing
   space, and NEVER the "margin: 0 0 X" shorthand: that sets an explicit top
   margin of 0, which beats this rule and makes the component touch whatever is
   above it (this is how notices used to sit flush under hints). If a block type
   must be a plain paragraph/div with no UA margin, add it to the zero-specificity
   reset just below; the rules after it still apply.

   THE GUARD. php tests/admin-ui-contract.php fails if this block is removed, and
   tests/admin-spacing measures real pages in a browser (see its README).
   Components that are DELIBERATELY flush (a toolbar fused to its editor) declare
   it with data-spacing-joined on the parent. */
/* Block components that have no top margin of their own. Zero specificity and
   declared BEFORE the rhythm rules, so those rules (and any explicit margin-top)
   take over wherever they apply. */
:where(.notice, .notice-warning, .error, .empty-state) {
    margin-top: 0;
}

:where(
    .admin-main,
    .admin-main .card:not(.stack, .stack--compact, .stack--loose, .form-grid),
    .admin-main form:not([class]),
    .admin-main details,
    .admin-main .flow
) > :where(
    :not(input[type="hidden"], [hidden], script, style, template)
    ~ :is(p, div, section, article, aside, header, footer, details, form, table, ul, ol, dl, fieldset, pre, figure, h2, h3, h4)
) {
    margin-top: var(--space-3);
}

:where(
    .admin-main,
    .admin-main .card:not(.stack, .stack--compact, .stack--loose, .form-grid),
    .admin-main form:not([class]),
    .admin-main details,
    .admin-main .flow
) > :where(
    :is(h2, h3, h4)
    + :is(p, div, section, article, details, form, table, ul, ol, dl, fieldset, pre, figure)
) {
    margin-top: var(--space-2);
}


/* ==========================================================================
   Cards / notices
   ========================================================================== */

.card {
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space-6);
    margin-bottom: var(--space-6);
    background: var(--surface-elevated);
    box-shadow: var(--shadow-card);
}

.card > h2 + .hint,
.card > .eyebrow + h2 + .hint,
.card > .card-heading-with-status + .hint {
    max-width: 760px;
}

/* A status describes a card heading; it must be a sibling in a deliberate
   layout rather than loose text inside an <h2>. This gives every CMS card
   the same reliable title-to-badge spacing and wrapping behaviour. */
.card-heading-with-status {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
}

.card-heading-with-status > h2 {
    margin: 0;
}

.card-heading-with-status + .hint {
    margin-top: var(--space-3);
}

/* Forms end with an explicit action row. Never place a bare button after
   a hint, status line, or final field: use this class so the final content
   and controls always have a stable, visible separation. */
.form-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-top: var(--space-4);
}

/* Generic composition primitives. Pages choose a semantic component first
   (.page-toolbar, .form-actions, .field-row); these cover small recurring
   arrangements without introducing one-off margins or flex declarations. */
.stack {
    display: grid;
    gap: var(--space-4);
}

.stack--compact {
    gap: var(--space-2);
}

.stack--loose {
    gap: var(--space-6);
}

/* A responsive grid for independently editable, contained form fields. */
.form-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: stretch;
    gap: var(--space-4);
}

.form-grid > .field--panel {
    min-width: 0;
    margin: 0;
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
}

.field--panel:focus-within {
    position: relative;
    z-index: 21;
}

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

/* Compact read-only facts belong in contained summary tiles, rather than as
   loose label/value text. This is shared by automation, integration and
   delivery status cards so values can never run into their labels. */
.status-summary-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(160px, 100%), 1fr));
    gap: var(--space-3);
}

.status-summary-grid--compact {
    grid-template-columns: repeat(auto-fit, minmax(min(130px, 100%), 1fr));
}

.status-summary-item {
    display: grid;
    align-content: start;
    gap: var(--space-2);
    min-width: 0;
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
}

.status-summary-label {
    color: var(--text-muted);
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.status-summary-value {
    color: var(--text);
    overflow-wrap: anywhere;
}

.status-summary-value code {
    color: inherit;
    font-size: 13px;
}

/* Template sections are explicitly separated. The preview follows its editor
   so the edit → result relationship is clear without sending an email. */
.template-editor {
    display: grid;
    gap: var(--space-4);
    margin-top: var(--space-5);
}

.template-editor + .template-editor {
    margin-top: var(--space-6);
    padding-top: var(--space-6);
    border-top: 1px solid var(--border);
}

.email-preview {
    display: grid;
    gap: var(--space-2);
}

.email-preview__label {
    color: var(--text-muted);
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.email-preview__window {
    overflow: hidden;
    border: 1px solid #4a3026;
    border-radius: var(--radius-md);
    background: #171312;
}

.email-preview__to,
.email-preview__subject,
.email-preview__body {
    margin: 0;
}

.email-preview__to {
    padding: var(--space-3) var(--space-5);
    border-bottom: 1px solid #4a3026;
    color: #b9a99c;
    font-size: 13px;
}

.email-preview__subject {
    margin: var(--space-5) var(--space-5) 0;
    color: #fff;
    font-size: 17px;
    font-weight: 700;
}

.email-preview__brand {
    margin: 0;
    padding: var(--space-4) var(--space-5);
    border-bottom: 3px solid #cf3d2d;
    background: #251711;
    color: #fff;
    font-size: 15px;
    font-weight: 800;
    letter-spacing: 0.08em;
}

.email-preview__body {
    min-height: 96px;
    margin: 0;
    padding: var(--space-5);
    color: #e7dcd1;
    line-height: 1.65;
    overflow-wrap: anywhere;
    white-space: pre-wrap;
}

.email-preview__footer {
    margin: 0;
    padding: var(--space-4) var(--space-5);
    border-top: 1px solid #4a3026;
    color: #a99b90;
    font-size: 12px;
    line-height: 1.5;
}

.button-group,
.toolbar-form {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-3);
}

/* A dropdown inside a button group is a control of normal width, level with the
   buttons around it. The global select rule is width: 100%, which would
   otherwise stretch it across the whole row and push the buttons onto their
   own lines. */
.button-group > select,
.bulk-actions > select,
.toolbar-form > select {
    flex: 0 1 auto;
    width: auto;
    min-width: 220px;
    max-width: 100%;
}

/* A dropdown inside a table cell has to be wide enough to show its chosen option. In a list row whose cell has a fixed width
   (Status, ...) the CELL decides: a 220px floor made a Status dropdown slide under the Save button in the next cell. */
td > select,
.list-cell.grow > select {
    min-width: 220px;
}
.list-cell:not(.grow) > select {
    min-width: 0;
}

/* A dropdown that saves itself (admin/news-reject-reasons.js) with its "Saved" text beside it.
   The slot for that text is reserved up front, so the text appearing never pushes anything or
   spills over the next column, and the cell has one stable width. */
.reason-cell {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.reason-cell > select {
    flex: 0 0 auto;
    /* A fixed, sensible width: left to its content it took the width of the longest reason (330px) and starved the
       story column beside it. */
    width: 12.5rem;
    min-width: 0;
}

.reason-cell > [data-reason-status] {
    flex: 0 0 4.75em;
    margin: 0;
    white-space: nowrap;
}

/* In a page toolbar the action group sits on the right and, when it has to
   wrap, wraps toward the right edge too. */
.page-toolbar > .button-group {
    justify-content: flex-end;
}

/* Filter/status controls form a complete block. When their result surface
   follows directly, the relationship owns the breathing room so pages do not
   need a one-off bottom margin on the controls or top margin on each table. */
.button-group + :is(.empty-state, .list-table, table, .pagination) {
    margin-top: var(--space-4);
}

.toolbar-form {
    margin: 0;
}

/* Mapping and Price Coverage lists: columns with a deliberate role, so nothing is squeezed and the controls on the right line up row after row.
   - .cell-stack: a value with its hint under it, on their own lines (inline, a hint ran on after the title and wrapped mid-sentence)
   - .w-decision: the mapping screen's Decision cell, wide enough for a game dropdown with its buttons on ONE line;
     .w-decision--buttons when the cell holds buttons only (Other editions, Mapped, Ignored)
   - .w-fix + .fix-actions: Price Coverage's cell, two rows (a Steam App ID field and its button; a reason dropdown and its button) whose
     first controls share one width, so both buttons start at the same place */
.list-cell.cell-stack {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-1);
}

.list-cell.cell-stack > :is(.hint, .list-value) {
    display: block;
    margin: 0;
}

/* The "Game" column of the news lists (Gamer Deer Articles, News Inbox): a game name that may wrap onto two lines, with a note under a guess. */
.list-cell.w-game { width: 11rem; flex-shrink: 0; }

.linked-game {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

/* Under 1180px the Game column would leave the title almost no room, so it is hidden and the game is shown under the title instead. */
.linked-games-inline {
    display: none;
}

@media (max-width: 1180px) {
    .list-cell.w-game {
        display: none;
    }

    .linked-games-inline {
        display: flex;
        flex-direction: column;
        gap: 2px;
    }
}

.linked-game + .linked-game {
    margin-top: var(--space-2);
}

/* A guess the site does not use (a game only mentioned in the headline, a low-confidence title match): visibly not a confirmed link. */
.linked-game--guess > a {
    color: var(--text-muted);
}

.list-cell.w-decision { width: 30rem; flex-shrink: 0; }
.list-cell.w-decision--buttons { width: 15rem; }
.list-cell.w-fix { width: 28rem; flex-shrink: 0; }

.fix-actions {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-3);
}

.fix-actions .toolbar-form,
.w-decision .toolbar-form {
    flex-wrap: nowrap;
}

/* The game dropdown / the field is a control of a set width, so the buttons beside it stay on its line instead of being pushed below. */
.fix-actions .toolbar-form > :is(input, select),
.w-decision .toolbar-form > select {
    flex: 0 0 12.5rem;
    width: 12.5rem;
    min-width: 0;
}

.fix-actions .toolbar-form > button,
.w-decision .toolbar-form > button {
    flex-shrink: 0;
    white-space: nowrap;
}

/* Narrower screens: the rows stay on one line, so these cells stop being fixed, share the room with the text columns, and let their controls
   drop to the next line instead of widening the row. */
@media (max-width: 1180px) {
    .list-cell.w-decision,
    .list-cell.w-decision--buttons,
    .list-cell.w-fix {
        width: auto;
        flex: 1 1 14rem;
    }

    .fix-actions .toolbar-form,
    .w-decision .toolbar-form {
        flex-wrap: wrap;
    }
}

/* A search form inside a page toolbar sits beside the page action on the right
   (count on the left, search, then the action) and keeps its input and buttons
   on one line. The global 100% input width would otherwise fill the form and
   push the Search button onto its own row. */
.page-toolbar > .toolbar-form {
    margin-left: auto;
    flex-wrap: nowrap;
}

.page-toolbar > .toolbar-form :is(input[type="search"], input[type="text"]) {
    flex: 0 1 300px;
    width: 300px;
    min-width: 0;
}

.toolbar-label {
    margin: 0;
    white-space: nowrap;
}

.filter-links {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin: 0;
}

.filter-links a[aria-current="page"] {
    color: var(--text);
    font-weight: 700;
}

.hint--flush {
    margin: 0;
}

.section-form {
    margin-top: var(--space-4);
}

.text-preserve-lines {
    white-space: pre-wrap;
}

/* Adjacent status, timestamp, and compact facts are data, not a sentence.
   Use this instead of relying on a space/newline between inline elements. */
.inline-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
}

.inline-meta > * {
    margin: 0;
}

.collector-evidence__details {
    display: grid;
    gap: var(--space-2);
}

/* Safety net for older forms while they are migrated to .form-actions. */
form > :is(.hint, .notice, .notice-warning) + :is(button, .button) {
    margin-top: var(--space-4);
}

.card > form {
    max-width: var(--content-measure);
    margin-top: var(--space-5);
}

.card > h2 + form {
    margin-top: var(--space-5);
}

/* Vertical rhythm for content that is rendered conditionally in PHP.
   These relationships cannot rely on a heading's default margin because
   the next element varies between helper text, a table, a list, a form,
   and an empty state. Keep the space on the relationship instead. */
.admin-main > .hint {
    max-width: var(--content-measure);
    margin: 0 0 var(--space-5);
}

/* Explanatory copy directly under a section heading must remain separated
   from whatever conditional block follows: empty state, table, form, or
   another status line. Put this class on the intro instead of styling each
   possible next element independently. */
.section-intro + * {
    margin-top: var(--space-4);
}

.card > h2 + *,
.card > .hint + :is(table, .list-table, .empty-state),
.admin-main > h2 + * {
    margin-top: var(--space-4);
}

/* Introductory card copy describes the form below it, but it is not a form
   label. Give the first field group a real separation everywhere, including
   legacy cards that have not yet adopted .form-stack. */
.card > .hint + :is(.field, .field-row, .form-stack) {
    margin-top: var(--space-5);
}

/* A direct action following supporting copy is common in CMS cards (for
   example, a refresh action after an empty-price message). Keep it visually
   separate without requiring every page to add a one-off wrapper. */
.card > :is(.hint, table, .list-table, .empty-state, .notice, .notice-warning) + :is(button, .button) {
    margin-top: var(--space-4);
}

.admin-main > .empty-state + h2 {
    margin-top: var(--space-7);
}

.card > :is(table, .list-table, .empty-state):last-child {
    margin-bottom: 0;
}

/* Trailing space only (see the vertical-rhythm note: no "margin: 0 0 X" shorthand,
   which would hard-code a zero top margin and glue the notice to what is above it). */
.notice {
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    color: var(--accent);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-md);
    margin-bottom: var(--space-5);
}

.notice-warning {
    background: var(--warning-soft);
    border: 1px solid var(--warning);
    color: var(--warning);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-md);
    margin-bottom: var(--space-5);
}

.error {
    background: var(--danger-soft);
    border: 1px solid var(--danger);
    color: #FBC4BE;
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-md);
    margin-bottom: var(--space-5);
}

/* ==========================================================================
   Status badges
   ========================================================================== */

/* A badge in the middle of a sentence ("PC Gamer · [Official] · score 42") needs room on its left as well. */
.hint .status-badge {
    margin-inline-start: var(--space-1);
}

.status-badge {
    display: inline-block;
    /* Room between a badge and the text beside it ("ON  While it is off..."): the space in the markup alone is only
       about 4px, which reads as touching next to a pill. */
    margin-inline-end: var(--space-2);
    padding: 4px 8px;
    border-radius: 999px;
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}
.status-published, .status-new { background: var(--accent-soft); color: var(--accent); }

/* A thin meter, e.g. today's AI budget on AI Providers. A native <progress> so no inline style is needed. */
.meter {
    display: block;
    width: 100%;
    height: 8px;
    margin: var(--space-3) 0 var(--space-4);
    border: 0;
    border-radius: 999px;
    overflow: hidden;
    background: var(--surface-3);
    accent-color: var(--accent);
    -webkit-appearance: none;
    appearance: none;
}
.meter::-webkit-progress-bar { background: var(--surface-3); }
.meter::-webkit-progress-value { background: var(--accent); border-radius: 999px; }
.meter::-moz-progress-bar { background: var(--accent); border-radius: 999px; }
.meter--full::-webkit-progress-value { background: var(--warning); }
.meter--full::-moz-progress-bar { background: var(--warning); }
.status-draft, .status-read { background: rgba(255, 255, 255, 0.08); color: var(--text-muted); }
.status-unpublished, .status-archived { background: var(--danger-soft); color: #F0968D; }
.status-stale { background: var(--warning-soft); color: var(--warning); }

/* Opportunity lifecycle is determined by both its schedule and the source's
   availability. Keep the title, source metadata, state and two dates as
   distinct blocks: an inline title + hint makes them visually run together. */
.opportunity-row__identity {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-1);
}

.opportunity-manual-section {
    margin: 0 0 var(--space-7);
}

.opportunity-manual-section > h2 {
    margin: 0;
}

.opportunity-manual-section > h2 + .hint {
    margin-top: var(--space-2);
}

.opportunity-card-heading {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
}

.opportunity-card-heading > h2 {
    margin: 0;
}

.opportunity-card-heading__action {
    flex-shrink: 0;
}

.card > .opportunity-card-heading + .hint {
    margin-top: var(--space-4);
}

.opportunity-row__title,
.opportunity-row__meta,
.opportunity-row__schedule > span {
    display: block;
}

.opportunity-row__title {
    font-weight: 600;
}

.opportunity-row__meta {
    margin: 0;
}

.opportunity-row__availability {
    display: flex;
    align-items: center;
}

.opportunity-row-main {
    margin-bottom: 0;
}

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

/* Opportunity rows carry several compact data columns, so a stronger divider
   and a gentle row highlight make each campaign easier to scan without
   competing with its availability/status badges. */
.opportunity-manual-section .list-table .list-row,
.opportunity-list .list-row {
    border-bottom: 1px solid var(--border-strong);
}

/* Automatic Opportunities is a data table, not a small form: let its
   selection form use the entire card so every schedule/status column has
   room to breathe. */
.card > form.opportunity-bulk-form {
    max-width: none;
}

.opportunity-manual-section .list-table .list-header + .list-row,
.opportunity-list .list-header + .list-row {
    border-top: 1px solid var(--border-strong);
}

.opportunity-manual-section .list-table .list-row + .list-row,
.opportunity-list .list-row + .list-row {
    border-top: 0;
}

.opportunity-manual-section .list-table .list-row:hover,
.opportunity-list .list-row:hover {
    background-color: rgba(255, 255, 255, 0.025);
}

.opportunity-row__action {
    display: inline-flex;
    box-sizing: border-box;
    flex: 0 0 68px;
    align-items: center;
    justify-content: center;
    width: 68px;
    min-width: 68px;
    height: 36px;
    min-height: 36px;
    padding: 7px 0;
    border: 1px solid rgba(74, 143, 209, 0.45);
    border-radius: var(--radius-sm);
    background: rgba(74, 143, 209, 0.1);
    color: #8FC5F7;
    font-family: inherit;
    font-size: 12px;
    font-weight: 600;
    line-height: 1;
    text-decoration: none;
    cursor: pointer;
    transition: background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}

.opportunity-row__action:hover {
    border-color: var(--btn-secondary-bg-hover);
    background: var(--btn-secondary-bg-hover);
    color: var(--btn-secondary-text-hover);
}

.opportunity-row__action--delete {
    border-color: rgba(214, 54, 56, 0.45);
    background: var(--danger-soft);
    color: #FF8F91;
}

.opportunity-row__action--delete:hover {
    border-color: var(--danger-hover);
    background: var(--danger-hover);
    color: var(--btn-danger-text-hover);
}

.opportunity-row__schedule {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 12px;
    line-height: 1.45;
}

.opportunity-status-summary {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-top: var(--space-4);
}

.opportunity-status-summary + .opportunity-list {
    margin-top: var(--space-4);
}

.opportunity-list__range {
    margin: var(--space-4) 0 0;
}

.opportunity-list__range + .list-table {
    margin-top: var(--space-3);
}

.opportunity-state {
    display: inline-block;
    padding: 4px 8px;
    border-radius: 999px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.04em;
    line-height: 1.2;
    text-transform: uppercase;
    white-space: nowrap;
}

.opportunity-state--active { background: var(--accent-soft); color: var(--accent); }
.opportunity-state--upcoming { background: var(--warning-soft); color: var(--warning); }
.opportunity-state--expired { background: rgba(255, 255, 255, 0.08); color: var(--text-muted); }
.opportunity-state--source-disabled { background: var(--danger-soft); color: #F0968D; }
.opportunity-state--draft { background: rgba(255, 255, 255, 0.08); color: var(--text-muted); }

/* Collector sources combine edit controls and operational health. Grouping
   them in two predictable panels prevents controls and run statistics from
   sprawling across the entire card on wide screens. */
.source-card__header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-5);
}

.source-card__identity {
    min-width: 0;
}

.source-card__identity h2,
.source-card__settings h3,
.source-card__activity h3 {
    margin: 0;
}

.source-card__endpoint {
    margin: var(--space-2) 0 0;
    color: var(--text-muted);
    font-size: 13px;
    overflow-wrap: anywhere;
}

.source-card__kind {
    color: var(--text-faint);
    font-family: var(--font-mono);
    text-transform: lowercase;
}

.source-card__form {
    margin-top: var(--space-5);
}

.source-card__content {
    display: grid;
    grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr);
    gap: var(--space-5);
}

.source-card__settings,
.source-card__activity {
    padding: var(--space-6);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
}

.source-card__toggles,
.source-card__number-fields {
    margin-top: var(--space-5);
}

.source-card__toggles {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-5);
}

.source-card__toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 28px;
    margin: 0;
    color: var(--text);
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    transition: color var(--transition-fast);
}

.source-card__toggle:hover {
    color: var(--accent);
}

.source-card__toggle input[type="checkbox"] {
    width: auto;
    margin: 0;
}

.source-card__number-fields {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-4);
}

.source-card__number-fields .field {
    margin: 0;
}

.source-card__unit {
    display: block;
    margin-top: var(--space-1);
    color: var(--text-faint);
    font-size: 12px;
    font-weight: 400;
    text-transform: none;
}

.source-card__timestamps {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-3);
    margin: var(--space-4) 0 0;
}

.source-card__timestamps div {
    min-width: 0;
}

.source-card__timestamps dt {
    color: var(--text-faint);
    font-size: 12px;
}

.source-card__timestamps dd {
    margin: var(--space-1) 0 0;
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 12px;
    overflow-wrap: anywhere;
}

.source-card__metrics {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--space-2);
    margin-top: var(--space-4);
}

.source-card__metrics span {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    padding: var(--space-2);
    border-radius: var(--radius-sm);
    background: var(--bg);
    color: var(--text-muted);
    font-size: 11px;
    text-transform: uppercase;
}

.source-card__metrics strong {
    color: var(--text);
    font-size: 16px;
    line-height: 1;
}

.source-card__last-run {
    display: grid;
    gap: var(--space-1);
    margin: var(--space-4) 0 0;
    color: var(--text-muted);
    font-size: 12px;
    line-height: 1.5;
}

.source-card__last-run strong {
    color: var(--text-faint);
    font-size: 11px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.source-card__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-top: var(--space-5);
    padding-top: var(--space-5);
    border-top: 1px solid var(--border);
}

/* Source rows are operational records rather than prose: the source name,
   trust context and endpoint need their own visual levels, while all actions
   stay on the same row instead of becoming a detached second line. */
.news-source__identity {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-1);
}

.news-source__title,
.news-source__meta,
.news-source__url {
    display: block;
    margin: 0;
}

.news-source__meta {
    line-height: 1.4;
}

.news-source__url {
    max-width: 100%;
    overflow-wrap: anywhere;
}

.list-cell.news-source__actions {
    flex: 0 0 160px;
    width: 160px;
    justify-content: flex-end;
    gap: var(--space-2);
    white-space: nowrap;
}

.list-cell.news-source__actions-heading {
    flex: 0 0 160px;
    width: 160px;
}

.news-source-list .list-row:hover {
    background: rgba(255, 255, 255, 0.025);
}

@media (max-width: 960px) {
    .source-card__content {
        grid-template-columns: 1fr;
    }
}

@media (max-width: 560px) {
    .source-card__header,
    .source-card__number-fields,
    .source-card__timestamps {
        grid-template-columns: 1fr;
    }

    .source-card__header {
        display: grid;
    }

    .source-card__metrics {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .source-card__actions > button {
        width: 100%;
    }
}

/* ==========================================================================
   Uploads / thumbnails
   ========================================================================== */

.upload-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
    /* Spacing above this comes from the .field > * + * rule, not a rule
       here — don't add margin-top on this class specifically. */
}

.upload-status {
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--text-faint);
}

.featured-image-control {
    display: grid;
    grid-template-columns: minmax(220px, 300px) minmax(0, 1fr);
    gap: var(--space-5);
    align-items: center;
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--bg);
}

.featured-image-preview {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    min-height: 180px;
    max-height: 280px;
    overflow: hidden;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-2);
}

.featured-image-preview.is-empty {
    border-style: dashed;
}

.featured-image-preview img {
    display: block;
    width: 100%;
    height: 100%;
    max-height: 280px;
    object-fit: contain;
}

.featured-image-preview span {
    color: var(--text-faint);
    font-size: var(--font-size-sm);
}

.featured-image-details {
    min-width: 0;
}

.featured-image-name {
    margin: 0;
    overflow-wrap: anywhere;
    color: var(--text);
    font-size: 14px;
    font-weight: 650;
}

.featured-image-meta {
    margin: var(--space-1) 0 0;
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 12px;
}

.featured-image-details .upload-row {
    flex-wrap: wrap;
    margin-top: var(--space-4);
}

.upload-button {
    margin: 0;
}

.upload-button.is-uploading {
    opacity: 0.65;
    cursor: wait;
    pointer-events: none;
}

.upload-remove[hidden] {
    display: none;
}

.featured-image-details .upload-status {
    display: block;
    min-height: 1.5em;
    margin-top: var(--space-3);
}

.upload-status.is-success {
    color: var(--accent);
}

.upload-status.is-error {
    color: var(--danger-hover);
}

/* ==========================================================================
   Submissions dashboard
   ========================================================================== */

/* align-items: flex-end, not center — a naked button (Search/Filter,
   no label above it) sits next to .field blocks that DO have a label
   above their input. Centering aligns boxes against the row's tallest
   item (the label+input field block), which puts the button's own box
   higher than the field's input specifically, since the input occupies
   only the bottom portion of that taller block, not its middle. Both a
   button and a field's input naturally share their BOTTOM edge already
   (the input is the last thing in its field, the button has nothing
   below it either), so flex-end is what actually reads as "aligned" to
   the eye — matching min-height/line-height between input and button
   (see the Forms/Buttons sections) only matters once this is right;
   neither fixes the other on its own. */
.submissions-toolbar,
.filter-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-3);
    margin-bottom: var(--space-4);
}

.filter-toolbar {
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
}

/* .field's own margin-bottom (20px, for stacking fields vertically
   elsewhere) has no business here — flexbox's align-items: flex-end
   aligns items by their MARGIN box, not their visible border box. To
   line up margin-box bottoms, the browser had to push the field's
   visible content (label + input) 20px ABOVE the shared alignment
   line, while the button (zero margin) sat right on that line — so
   the button rendered 20px lower than the input, hanging visibly
   past its bottom edge. Worse than the earlier center-alignment
   mismatch, not better. Zeroing the margin here makes the field's
   margin box equal its border box, so flex-end aligns the actual
   visible edges instead. */
.submissions-toolbar .field,
.filter-toolbar .field {
    margin-bottom: 0;
}

.filter-toolbar select {
    min-width: 150px;
}

.filter-toolbar input[type="text"] {
    min-width: 220px;
}

.submissions-toolbar select {
    width: auto;
}

tr.submission-detail td {
    background: var(--bg);
    border-bottom: 1px solid var(--border);
}

.submission-detail-body {
    padding: var(--space-3) 0;
}

.submission-detail-body > * + * {
    margin-top: var(--space-4);
}

/* Small pill for contextual metadata (source page, form name) shown above
   a submission's field values — not a status, so deliberately not built
   from .status-badge. */
.chip {
    display: inline-block;
    background: var(--surface-2);
    border: 1px solid var(--border);
    color: var(--text-muted);
    padding: 4px 10px;
    border-radius: var(--radius-sm);
    font-size: 12px;
    margin-right: var(--space-2);
}

.chip strong {
    color: var(--text);
}

.submission-fields {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: var(--space-4);
}

.submission-field {
    /* Flex items default to min-width:auto, so a value with no spaces
       (a long URL, a run-on word) refuses to shrink below its own
       intrinsic width and pushes .submission-fields, the table, and the
       whole admin page wider: same fix as .gallery-item-body above. */
    min-width: 0;
}

.submission-field-label {
    margin: 0 0 4px;
    color: var(--accent);
    font-size: 13px;
    font-weight: 600;
}

.submission-field p:last-child {
    margin: 0;
    overflow-wrap: break-word;
}

/* Square icon-only button that flips the chevron when its row is open —
   sits in the same rail as the other row actions, so it's sized like one. */
.expand-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    padding: 0;
    background: var(--surface-2);
    border: 1px solid var(--border);
    color: var(--text-muted);
}

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

.expand-toggle[aria-expanded="true"] svg {
    transform: rotate(180deg);
}

.thumb-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    /* Spacing above this comes from the .field > * + * rule, not a rule
       here — don't add margin-top on this class specifically. */
}

.thumb {
    height: 60px;
    width: 60px;
    object-fit: cover;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
}

/* The single featured-image preview (not the gallery grid, which stays
   small/cropped via .thumb above) — big enough to actually judge the
   image by, and never cropped: width/height:auto inside a max-size box
   preserves the source image's real aspect ratio. */
.thumb-lg {
    display: block;
    max-width: 320px;
    max-height: 320px;
    width: auto;
    height: auto;
    object-fit: contain;
    border-radius: var(--radius-md);
    border: 1px solid var(--border);
    background: var(--surface-2);
}

.thumb-sm {
    height: 36px;
    width: 36px;
    object-fit: cover;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    display: block;
}

/* ==========================================================================
   Dashboard stat cards
   ========================================================================== */

.stat-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--space-4);
    margin-bottom: var(--space-7);
}

.stat-card {
    display: block;
    position: relative;
    min-width: 0;
    padding: var(--space-5) var(--space-6);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--surface-elevated);
    text-decoration: none;
    color: var(--text);
    box-shadow: var(--shadow-card);
    transition: border-color var(--transition-fast), background-color var(--transition-fast), transform var(--transition-fast);
}

.stat-card::after {
    content: '→';
    position: absolute;
    top: var(--space-5);
    right: var(--space-5);
    color: var(--text-faint);
    font-size: 17px;
    transition: color var(--transition-fast), transform var(--transition-fast);
}

.stat-card:hover {
    border-color: var(--accent);
    background: var(--surface-3);
    color: var(--text);
    transform: translateY(-2px);
}

.stat-card:hover::after {
    color: var(--accent);
    transform: translateX(2px);
}

.stat-card.attention {
    border-color: rgba(76, 183, 49, 0.45);
    background: linear-gradient(135deg, var(--surface-elevated), var(--accent-soft));
}

.stat-card .stat-number {
    display: block;
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 32px;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: var(--accent);
}

.stat-card .stat-label {
    display: block;
    font-size: 14px;
    color: var(--text-muted);
    margin-top: var(--space-1);
}

/* ==========================================================================
   Login
   ========================================================================== */

.login-wrap {
    max-width: 340px;
    margin: 100px auto;
    padding: var(--space-6);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
}

.login-wrap h1 {
    margin-bottom: var(--space-5);
}

/* ==========================================================================
   Modal (deploy output — the first modal in this admin, kept generic
   enough to reuse for anything else that needs one later)
   ========================================================================== */

.modal {
    position: fixed;
    inset: 0;
    background: rgb(0 0 0 / 60%);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-6);
    z-index: 100;
}

/* Author CSS (the display: flex above) beats the browser's own UA
   stylesheet regardless of selector order, so [hidden] alone wouldn't
   actually hide this without an explicit override here. */
.modal[hidden] {
    display: none;
}

.modal-content {
    width: 100%;
    max-width: 640px;
    max-height: 80vh;
    overflow-y: auto;
    padding: var(--space-5);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
}

/* Always-visible deploy monitor (admin/index.php's Deploy card) — replaces
   the old modal-only view, which only ever showed anything for the
   duration of the tab that clicked the button. This panel loads the most
   recent run's status/output on every Dashboard visit (admin/push-local.js
   polls it), so a deploy started earlier — or one that finished five
   minutes ago — is still visible on a plain page refresh. */
.deploy-actions {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin-top: var(--space-4);
}

.deploy-actions button {
    gap: var(--space-2);
}

/* The generic button rule sets `display`, and author CSS beats the browser's
   own [hidden] rule, so without this the Stop button (hidden until a deploy
   is running) shows while idle. Same trap as .modal[hidden] above. */
.deploy-actions button[hidden] {
    display: none;
}

.deploy-actions button svg {
    width: 16px;
    height: 16px;
    flex-shrink: 0;
}

.deploy-monitor {
    margin-top: var(--space-4);
}

.deploy-monitor-status {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: 14px;
}

.deploy-status-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    flex-shrink: 0;
    background: var(--text-muted);
}

.deploy-status-dot[data-state="running"] {
    background: var(--warning);
    animation: deploy-status-pulse 1.4s ease-in-out infinite;
}

.deploy-status-dot[data-state="success"] {
    background: var(--accent);
}

.deploy-status-dot[data-state="error"] {
    background: var(--danger);
}

@keyframes deploy-status-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.35; }
}

.deploy-status-time {
    color: var(--text-muted);
    font-size: 12px;
    margin-left: auto;
}

.deploy-monitor-output {
    margin-top: var(--space-3);
    padding: var(--space-3);
    max-height: 50vh;
    overflow-y: auto;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    font-family: var(--font-mono);
    font-size: 13px;
    white-space: pre-wrap;
    word-break: break-word;
}

.deploy-monitor-output[hidden] {
    display: none;
}

.deploy-monitor-output.deploy-output-error {
    border-color: var(--danger);
    color: var(--danger-hover);
}

.deploy-monitor-history {
    margin-top: var(--space-4);
}

.deploy-monitor-history[hidden] {
    display: none;
}

.deploy-monitor-history ul {
    margin-top: var(--space-2);
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.deploy-history-item {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    width: 100%;
    padding: var(--space-2) var(--space-3);
    background: none;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text);
    font-size: 13px;
    text-align: left;
    cursor: pointer;
}

.deploy-history-item:hover {
    border-color: var(--accent);
}

.deploy-history-item.is-active {
    border-color: var(--accent);
    background: var(--surface-2);
}

.deploy-history-item .deploy-status-dot {
    width: 8px;
    height: 8px;
}

.deploy-history-target {
    flex-shrink: 0;
    padding: 2px 6px;
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.03em;
}

.deploy-history-target-staging,
.deploy-history-target-dry-run {
    background: var(--surface-2);
    color: var(--text-muted);
}

/* Red, not the neutral staging treatment — this is the one badge in the
   history list that means "this run touched the real, public site," and
   it needs to read as such at a glance while scanning past entries. */
.deploy-history-target-production {
    background: var(--danger);
    color: #FFFFFF;
}

.deploy-history-item-time {
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 12px;
    margin-left: auto;
}

/* ==========================================================================
   Refined workspace components
   ========================================================================== */

.dashboard-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-5);
    margin-bottom: var(--space-7);
}

.dashboard-grid .card {
    margin-bottom: 0;
}

.dashboard-actions > .eyebrow {
    margin-bottom: var(--space-1);
}

.dashboard-actions > .hint {
    margin-top: var(--space-2);
}

.quick-action-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-2);
    margin-top: var(--space-5);
}

.quick-action {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-2);
    color: var(--text);
    transition: border-color var(--transition-fast), background-color var(--transition-fast), transform var(--transition-fast);
}

.quick-action:hover {
    border-color: var(--accent);
    background: var(--surface-3);
    color: var(--text);
    transform: translateY(-1px);
}

.quick-action span {
    font-size: 14px;
    font-weight: 650;
}

.quick-action small {
    overflow: hidden;
    color: var(--text-muted);
    font-size: 12px;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.submissions-toolbar {
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
}

.submissions-toolbar select {
    min-width: 150px;
}

.chip {
    border-radius: 999px;
}

.submission-fields {
    border-radius: var(--radius-md);
    background: var(--surface);
}

.gallery-item {
    padding: var(--space-4);
    border-radius: var(--radius-md);
    background: var(--bg);
}

.gallery-item-thumb {
    border-radius: var(--radius-md);
}

.login-wrap {
    max-width: 400px;
    margin: 12vh auto;
    padding: var(--space-7);
    border-radius: var(--radius-lg);
    background: var(--surface-elevated);
    box-shadow: var(--shadow-card);
}

.modal {
    backdrop-filter: blur(6px);
}

.modal-content {
    border-radius: var(--radius-lg);
    background: var(--surface-elevated);
    box-shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
}

/* The workspace narrows first; phones use a collapsible navigation header. */
@media (max-width: 1180px) {
    .admin-main {
        padding: var(--space-7) var(--space-7) var(--space-9);
    }

    .list-header,
    .list-row-main {
        gap: var(--space-4);
    }

    .dashboard-grid {
        grid-template-columns: 1fr;
    }
}

@media (max-width: 960px) {
    .admin-main {
        padding: var(--space-6);
    }

    .page-header {
        margin-bottom: var(--space-6);
    }

    .stat-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    /* Phones: fields stack one per line, so the shared label/control rows are not needed. */
    .field-row {
        display: flex;
        flex-direction: column;
        gap: 0;
    }

    .form-toggle-group {
        grid-template-columns: 1fr;
    }

    .field-row .field,
    .field-row > .field {
        display: block;
        width: 100%;
        margin-bottom: var(--space-4);
    }

    .featured-image-control {
        grid-template-columns: 1fr;
        align-items: stretch;
    }

    .list-table {
        overflow-x: auto;
    }

    .list-header,
    .list-row {
        min-width: 780px;
    }

    /* This list has more columns than the 780px default can hold; without this its
       cells run out past the row borders while the table scrolls sideways. */
    .opportunity-list .list-header,
    .opportunity-list .list-row {
        min-width: 1100px;
    }

    table {
        display: block;
        max-width: 100%;
        overflow-x: auto;
    }

    .gallery-item {
        flex-wrap: wrap;
    }

    .gallery-item-body {
        flex-basis: calc(100% - 180px);
    }

    .gallery-item-actions {
        flex-direction: row;
        width: 100%;
    }

    .gallery-item-actions button,
    .gallery-item-actions .button {
        flex: 1;
        min-height: 38px;
    }
}

@media (max-width: 720px) {
    .admin-layout {
        flex-direction: column;
    }

    .admin-sidebar {
        width: 100%;
        min-height: 0;
        padding: var(--space-4) var(--space-5);
        border-right: 0;
        border-bottom: 1px solid var(--border);
    }

    .sidebar-heading {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: var(--space-3);
    }

    .sidebar-heading .admin-brand {
        padding: 0;
        font-size: 16px;
    }

    .sidebar-menu-toggle {
        display: inline-flex;
    }

    .admin-sidebar:not(.is-menu-open) > :is(nav, .sidebar-version, .sidebar-footer) {
        display: none;
    }

    .admin-sidebar.is-menu-open > nav {
        margin-top: var(--space-4);
    }

    .form-actions > :is(button, .button, form) {
        max-width: 100%;
    }

    .form-actions > :is(button, .button),
    .form-actions > form > button {
        height: auto;
        min-height: var(--control-height);
        white-space: normal;
    }

    .admin-main {
        padding: var(--space-5);
    }

    .page-header {
        padding-bottom: var(--space-5);
    }

    .section-heading {
        align-items: flex-start;
        flex-direction: column;
        gap: var(--space-1);
    }

    .stat-grid,
    .quick-action-grid {
        grid-template-columns: 1fr;
    }


    .card {
        padding: var(--space-5);
    }

    .page-toolbar {
        align-items: flex-start;
        flex-direction: column;
    }

    .submissions-toolbar,
    .filter-toolbar,
    .toolbar-form {
        align-items: stretch;
        flex-direction: column;
    }

    .page-toolbar > .toolbar-form {
        align-self: stretch;
        margin-left: 0;
    }

    .page-toolbar > .toolbar-form :is(input[type="search"], input[type="text"]),
    .list-search input[type="search"] {
        flex: none;
        width: 100%;
    }

    .list-filter-bar {
        display: block;
    }

    .pagination-jump {
        padding-left: 0;
        border-left: 0;
    }

    .submissions-toolbar select,
    .submissions-toolbar .button,
    .submissions-toolbar button,
    .filter-toolbar select,
    .filter-toolbar input[type="text"],
    .filter-toolbar .button,
    .filter-toolbar button {
        width: 100%;
    }

    .gallery-item-thumb {
        width: 100%;
        height: auto;
        max-height: 280px;
    }

    .gallery-item-body {
        flex-basis: 100%;
    }

    .gallery-item-actions {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

/* ==========================================================================
   Media Library (admin/media.php, admin/media-picker.php)
   ========================================================================== */

.media-toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-bottom: var(--space-5);
}

.media-toolbar input[type="text"] {
    flex: 2;
    min-width: 0;
}

.media-toolbar select {
    flex: 1;
    min-width: 0;
}

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

.media-card-wrap {
    position: relative;
    min-width: 0;
}

.media-card-select {
    position: absolute;
    z-index: 1;
    top: var(--space-2);
    left: var(--space-2);
    margin: 0;
    padding: var(--space-1);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--surface-elevated);
    line-height: 1;
    cursor: pointer;
    transition: all var(--transition-fast);
}

.media-card-select:focus-within,
.media-card-select:hover {
    border-color: var(--accent);
}

.media-card-select input {
    width: auto;
    margin: 0;
}

/* Doubles as an <a> (admin/media.php's list) and a <button> (the
   chrome-less picker) — reset to plain-block either way so both read as
   the same card regardless of which element renders it. */
.media-card {
    display: flex;
    flex-direction: column;
    /* The global `button, .button { height: var(--control-height) }` form-control rule
       (see the Buttons section) matches the media-picker.php variant of
       this card, since that page renders it as a real <button> — without
       this override every card gets force-squashed to a fixed-height form-button
       height, clipping the thumbnail and filename down to almost nothing
       and leaving only the size line visibly readable. The <a> variant
       (admin/media.php) was never affected since `button`/`.button` don't
       match an anchor, so this only needs to reset height back to auto,
       not min-height too. */
    height: auto;
    /* Grid items default to min-width: auto, i.e. never narrower than their
       content's min-content size — a nowrap filename's full text width, in
       this case. That silently overrides the grid track's own 150px/1fr
       sizing and pushed the card (and everything after it) wider than its
       cell, which is what let the filename spill into neighboring cards
       despite .media-card-name's own overflow/ellipsis rules below. */
    min-width: 0;
    /* min-width:0 alone was enough for the <a> variant (admin/media.php),
       but the media-picker.php variant renders this as a <button> — form
       controls default to sizing themselves to their own content instead
       of stretching to fill their grid cell the way a plain block element
       does, so without an explicit width the button still grew to fit the
       filename regardless of min-width. */
    width: 100%;
    max-width: 100%;
    gap: var(--space-2);
    padding: var(--space-3);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    text-align: left;
    cursor: pointer;
    transition: border-color var(--transition-fast);
}

.media-card:hover {
    border-color: var(--border-strong);
}

.media-card-thumb {
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    border-radius: var(--radius-sm);
    background: var(--surface-2);
}

.media-card-thumb img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.media-card-icon {
    color: var(--text-muted);
}

.media-card-name {
    min-width: 0;
    width: 100%;
    font-size: 12px;
    color: var(--text);
    overflow: hidden;
    white-space: normal;
}

.media-card-meta {
    font-size: 11px;
    color: var(--text-muted);
    text-align: left;
    width: 100%;
}

.media-detail {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-6);
    align-items: start;
}

.media-detail-preview img,
.media-detail-preview video {
    display: block;
    width: 100%;
    height: auto;
    border-radius: var(--radius-md);
    background: var(--surface-2);
}

.media-detail-info p {
    margin-top: var(--space-4);
}

.media-detail-info p:first-child {
    margin-top: 0;
}

.media-detail-info ul {
    margin-top: var(--space-2);
    padding-left: var(--space-5);
    color: var(--text-muted);
}

@media (max-width: 720px) {
    .media-detail {
        grid-template-columns: 1fr;
    }
}

/* The chrome-less picker page loaded inside media-picker-frame below —
   its own minimal <body>, not admin_header()/admin_footer()'s sidebar
   layout. */
.media-picker-body {
    background: var(--bg);
    color: var(--text);
    font-family: var(--font-ui);
    padding: var(--space-5);
}

.media-picker-modal-content {
    width: 100%;
    max-width: 900px;
    max-height: 85vh;
    display: flex;
    flex-direction: column;
}

.media-picker-modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: var(--space-4);
}

.media-picker-frame {
    width: 100%;
    height: 70vh;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
}

/* Request status edits keep the select and action aligned, while the
   request tables retain every column through a horizontal scroll rail on
   narrow admin viewports. */
.request-status-control {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.request-status-control select,
.request-status-control button {
    margin: 0;
}

.table-scroll {
    /* Positioned so that absolutely positioned cell content (the visually hidden
       switch checkboxes) is contained and clipped here, instead of stretching the
       whole page sideways on a phone. */
    position: relative;
    overflow-x: auto;
}

.table-scroll .integrations-table {
    min-width: 0;
    margin-bottom: 0;
}
/* A table with several editable text fields per row (the verification tags): each field keeps a sensible floor, not the 200px
   of a single-field table, so four of them still fit the card. */
.integrations-table--compact td > input:is([type="text"], [type="url"], [type="email"], [type="search"], :not([type])) {
    min-width: 5.5rem;
}
.integrations-table--compact td > select {
    min-width: 0;
}

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

.integration-system-value span,
.integration-toggle-only {
    color: var(--text-faint);
    font-size: 12px;
}

/* ==========================================================================
   IGDB search/preview (admin/games-import.php, admin/games.php)
   ========================================================================== */

.igdb-result-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: var(--space-4);
    margin: var(--space-5) 0;
}

.igdb-result-card {
    display: flex;
    gap: var(--space-3);
    padding: var(--space-3);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
}

.igdb-result-cover {
    flex-shrink: 0;
    width: 60px;
    height: 80px;
    object-fit: cover;
    border-radius: var(--radius-sm);
    background: var(--surface-2);
}

.igdb-result-cover--empty {
    border: 1px dashed var(--border);
}

.igdb-result-info {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    min-width: 0;
}

.igdb-result-title {
    margin: 0;
    font-weight: 600;
}

.igdb-result-meta {
    margin: 0;
    color: var(--text-muted);
    font-size: 12px;
}

/* Preview / Add to site: equal-width columns side by side, stacking to
   full-width equal rows when the card is too narrow for both. */
.igdb-result-actions {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
}

.igdb-result-actions > :is(button, .button) {
    width: 100%;
}

.igdb-preview-header {
    display: flex;
    gap: var(--space-5);
    margin-bottom: var(--space-5);
}

.igdb-preview-cover {
    flex-shrink: 0;
    width: 140px;
    height: 187px;
    object-fit: cover;
    border-radius: var(--radius-sm);
    background: var(--surface-2);
}

.igdb-preview-line {
    margin: 0 0 var(--space-1);
    font-size: 13px;
}

.igdb-image-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
    gap: var(--space-3);
    margin: 0 0 var(--space-5);
}

.igdb-image-grid-item {
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
    border-radius: var(--radius-sm);
    background: var(--surface-2);
}

.igdb-preview-actions {
    margin-top: var(--space-5);
}

/* News body block editor (admin/news.php + admin/news-block-editor.js):
   Heading/Paragraph/Image/Quote blocks, drag-reordered by their handle.
   Sits inside a .field, so it and .block-editor__add-row get their own
   spacing for free from the standard .field > * + * rule — nothing here
   should ever add a margin-bottom for that reason. */
.block-editor {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.block-editor__block {
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
    padding: var(--space-4);
}

.block-editor__block.is-dragging {
    opacity: 0.4;
}

.block-editor__row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
}

.block-editor__handle {
    cursor: grab;
    color: var(--text-muted);
    line-height: 1;
    letter-spacing: -2px;
    user-select: none;
}

.block-editor__handle:active {
    cursor: grabbing;
}

.block-editor__label {
    flex: 1;
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}

.block-editor__remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    padding: 0;
    border: none;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--text-muted);
    font-size: 18px;
    line-height: 1;
    cursor: pointer;
    transition: color var(--transition-fast), background-color var(--transition-fast);
}

.block-editor__remove:hover {
    background: var(--danger-soft);
    color: var(--danger-hover);
}

.block-editor__block input[type="text"],
.block-editor__block textarea {
    width: 100%;
}

.block-editor__add-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
}

/* ==========================================================================
   AI Learning (News > News Agent) and the notification banner
   ========================================================================== */

.ai-learning > .card {
    margin-bottom: var(--space-6);
}

.learning-na {
    color: var(--text-faint);
    font-style: italic;
}

.learning-reasons {
    margin: var(--space-3) 0 var(--space-4);
    padding-left: var(--space-6);
    color: var(--text-muted);
}

.learning-reasons li + li {
    margin-top: var(--space-2);
}

.learning-missing-title {
    margin: var(--space-4) 0 var(--space-2);
    font-weight: 600;
}

/* The five questions: a grid of tiles that wraps to one column on a phone. */
.learning-tiles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
    gap: var(--space-4);
    margin-top: var(--space-4);
}

.learning-tile {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    min-width: 0;
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-left-width: 3px;
    border-radius: var(--radius-md);
    background: var(--surface-2);
}

.learning-tile--ok { border-left-color: var(--accent); }
.learning-tile--warn { border-left-color: var(--warning); }
.learning-tile--bad { border-left-color: var(--danger); }

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

.learning-tile__value {
    font-size: 18px;
    font-weight: 600;
    overflow-wrap: break-word;
}

.learning-tile__detail {
    font-size: var(--font-size-sm);
    color: var(--text-muted);
    overflow-wrap: break-word;
}

/* A definition list laid out as label / value pairs. */
.learning-facts {
    display: grid;
    gap: var(--space-3);
    margin: var(--space-4) 0 0;
}

.learning-facts > div {
    display: grid;
    grid-template-columns: minmax(150px, 220px) minmax(0, 1fr);
    gap: var(--space-4);
}

.learning-facts dt {
    color: var(--text-muted);
}

.learning-facts dd {
    margin: 0;
    overflow-wrap: anywhere;
}

/* Training progress: one labelled bar per requirement. */
.learning-bars {
    display: grid;
    gap: var(--space-4);
    margin: var(--space-4) 0;
}

.learning-bar__head {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: var(--space-2) var(--space-4);
}

.learning-bar__value {
    color: var(--text-muted);
}

.learning-bar .meter {
    margin: var(--space-2) 0 0;
}

.meter--partial::-webkit-progress-value { background: var(--warning); }
.meter--partial::-moz-progress-bar { background: var(--warning); }

/* Candidate comparison. */
.learning-candidate {
    margin-top: var(--space-5);
    padding: var(--space-5);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-2);
}

.learning-candidate--waiting {
    border-color: var(--warning);
}

.learning-candidate h3,
.learning-candidate h4 {
    margin: 0;
}

.learning-candidate h4 {
    margin: var(--space-4) 0 var(--space-3);
}

.learning-verdict-reason {
    margin: var(--space-3) 0;
}

.learning-compare th,
.learning-compare td,
.learning-history th,
.learning-history td {
    vertical-align: top;
}

.learning-compare td {
    font-family: var(--font-mono);
    white-space: nowrap;
}

.learning-compare thead th,
.learning-history thead th {
    white-space: nowrap;
}

.learning-compare .learning-better { color: var(--accent); }
.learning-compare .learning-worse { color: #F0968D; }

.learning-more {
    margin-top: var(--space-4);
}

.learning-more > summary {
    cursor: pointer;
    color: var(--text-muted);
    transition: color var(--transition-fast);
}

.learning-more > summary:hover {
    color: var(--text);
}

.learning-approve {
    margin-top: var(--space-5);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border);
}

.learning-approve input {
    width: 100%;
}

/* Approve, Reject and Roll back sit in their own row under the explanation, with room above them. */
.learning-approve__buttons {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin: var(--space-3) 0 var(--space-4);
}

/* The open model request in the Model changes card: the same box as a candidate, with the warning border while it is in flight. */
.learning-request {
    margin: var(--space-5) 0;
    padding: var(--space-5);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-2);
}

.learning-request--open {
    border-color: var(--warning);
}

.learning-request h3 {
    margin: 0;
}

.learning-request form {
    margin-top: var(--space-4);
}

/* The "what happened" column of a request is prose: give it room before it wraps. */
.learning-changes td.col-fill {
    min-width: 220px;
}

/* On a phone the model-change tables become lists of small cards (as the AI Picks table does): the request first, then what happened, then the small
   facts each with its label, so nothing important sits off the edge of the screen. */
@media (max-width: 720px) {
    .table-scroll .learning-changes,
    .table-scroll .learning-changes tbody {
        display: block;
        width: 100%;
    }

    .table-scroll .learning-changes thead {
        display: none;
    }

    .table-scroll .learning-changes tr {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-3) var(--space-5);
        padding: var(--space-4) 0;
        border-bottom: 1px solid var(--border);
    }

    .table-scroll .learning-changes tr:last-child {
        border-bottom: 0;
    }

    .table-scroll .learning-changes td {
        display: block;
        width: auto;
        min-width: 0;
        padding: 0;
        border: 0;
    }

    .table-scroll .learning-changes td.col-title {
        order: -2;
        flex: 1 1 100%;
        font-weight: 600;
        overflow-wrap: anywhere;
    }

    .table-scroll .learning-changes td.col-fill {
        order: -1;
        flex: 1 1 100%;
        min-width: 0;
        overflow-wrap: break-word;
    }

    .table-scroll .learning-changes td[data-label]:not(.col-fill):not(.col-title)::before {
        content: attr(data-label);
        display: block;
        margin-bottom: var(--space-1);
        font-family: var(--font-mono);
        font-size: 11px;
        letter-spacing: 0.04em;
        text-transform: uppercase;
        color: var(--text-faint);
    }
}

.learning-refresh-note {
    margin-bottom: var(--space-4);
}

/* The notification banner, on every live admin page. */
.admin-notifications {
    display: grid;
    gap: var(--space-3);
    margin-bottom: var(--space-6);
}

.admin-notification {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3) var(--space-5);
    padding: var(--space-4);
    border: 1px solid var(--warning);
    border-radius: var(--radius-md);
    background: var(--warning-soft);
}

.admin-notification--critical {
    border-color: var(--danger);
    background: var(--danger-soft);
}

.admin-notification--info {
    border-color: var(--border-strong);
    background: var(--surface-2);
}

.admin-notification__text {
    display: flex;
    flex: 1 1 320px;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
}

.admin-notification__text span {
    color: var(--text-muted);
    overflow-wrap: break-word;
}

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

.admin-notification__actions form {
    margin: 0;
}

@media (max-width: 640px) {
    .learning-facts > div {
        grid-template-columns: 1fr;
        gap: var(--space-1);
    }

    .learning-candidate {
        padding: var(--space-4);
    }
}

/* AI Picks */
.picks-table td.col-compact,
.picks-table th.col-compact {
    vertical-align: top;
}

.picks-table td.col-fill .hint {
    display: block;
    margin-top: var(--space-1);
}

.cell-note.picks-agree { color: var(--accent); }
.cell-note.picks-differ { color: var(--warning); }

/* The AI's rank for a story: a big "#3", green inside the day's Top 10, amber when it ranked a story low. */
.picks-rank {
    display: block;
    font-family: var(--font-mono);
    font-size: 16px;
    line-height: 1.3;
    color: var(--text);
}

.picks-rank--high { color: var(--accent); }
.picks-rank--low { color: var(--warning); }

/* The separate "Possible duplicate" warning sits under the story's source, with room above it. */
.picks-table td.col-fill .picks-dup {
    display: inline-block;
    margin-top: var(--space-2);
}

/* One card per day in the daily Top 5 / Top 10 views: the same card rhythm as every other card, the table runs to its edges. */
.picks-day .table-scroll {
    margin-top: var(--space-3);
}

/* Filter tabs that live INSIDE a card (the Top 5 / Top 10 choice): room above them, and the card's own padding closes the bottom. */
.picks-card-tabs {
    margin-top: var(--space-3);
    margin-bottom: 0;
}

/* The two legend lines (the AI's column, then yours) are one block with room below it, so the line after never touches it. */
.picks-legend-block {
    margin-bottom: var(--space-4);
}

.picks-legend-block .picks-legend {
    margin-bottom: var(--space-2);
}

/* The "stories it publishes a day" control: a number field and its buttons on one row, with room above them. */
.picks-n-form {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-3) var(--space-4);
    margin: var(--space-4) 0;
}

.picks-n-form .field {
    margin: 0;
}

.picks-n-form input[type="number"] {
    width: 96px;
}

.picks-n-form .form-actions {
    margin: 0;
}

/* The legend under the sub-views: badges are inline, so give them room to breathe between the words. */
.picks-legend .status-badge {
    margin: 0 var(--space-1);
}

/* On a phone the AI Picks table becomes a list of small cards: the story first, then when, what the AI says and what you did, each with its label. */
@media (max-width: 720px) {
    .table-scroll .picks-table,
    .table-scroll .picks-table tbody {
        display: block;
        width: 100%;
    }

    .table-scroll .picks-table thead {
        display: none;
    }

    .table-scroll .picks-table tr {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-3) var(--space-5);
        padding: var(--space-4) 0;
        border-bottom: 1px solid var(--border);
    }

    .table-scroll .picks-table tr:last-child {
        border-bottom: 0;
    }

    .table-scroll .picks-table td {
        display: block;
        width: auto;
        min-width: 0;
        padding: 0;
        border: 0;
    }

    .table-scroll .picks-table td.col-fill {
        order: -1;
        flex: 1 1 100%;
        min-width: 0;
        overflow-wrap: break-word;
    }

    .table-scroll .picks-table td[data-label]:not(.col-fill)::before {
        content: attr(data-label);
        display: block;
        margin-bottom: var(--space-1);
        font-family: var(--font-mono);
        font-size: 11px;
        letter-spacing: 0.04em;
        text-transform: uppercase;
        color: var(--text-faint);
    }
}
