:root {
    --bg: #0f1115;
    --panel: #171a21;
    --panel-2: #1f232c;
    --text: #e7e9ee;
    --muted: #9aa3b2;
    --accent: #6ea8fe;
    --line: #2a2f3a;
    /* M88 S4: flash/status theme tokens (border + panel tint) — success, error, info. */
    --ok: #2f6b3f; --ok-bg: #14251a;
    --warn: #7a2f2f; --warn-bg: #2a1616;
    --info: #2f4f6b; --info-bg: #14202a;
}

* { box-sizing: border-box; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font: 15px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

main { max-width: 1100px; margin: 0 auto; padding: 24px 20px 64px; }

.error-page { max-width: 560px; }
.error-code { margin: 56px 0 4px; color: var(--accent); font-size: 13px; font-weight: 700; letter-spacing: .12em; }
.error-page h2 { margin: 0 0 8px; font-size: 24px; }

/* ---- gallery ------------------------------------------------------------ */
.gallery-head { display: flex; align-items: baseline; gap: 14px; }
.gallery-head h1, .archive-head h1 { margin: 0 0 16px; font-size: 24px; }
.gallery-head h1 a { color: var(--text); }
/* G3 (M72): the file-count badge beside the wordmark — muted and smaller than the brand. */
.gallery-count { color: var(--muted); font-size: 15px; font-weight: 400; }
.gallery-admin-link, .gallery-albums-link { color: var(--accent); font-size: 13px; }
.gallery-admin-link:hover, .gallery-albums-link:hover { text-decoration: underline; }
.gallery-albums-heading { color: var(--muted); font-size: 15px; font-weight: 400; }

/* The public albums list (M83 U2, V33): mosaic cards, one column on the phone (D6). */
.public-album-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; margin: 16px 0 24px;
}
.public-album-card {
    display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: 10px;
    background: var(--panel); overflow: hidden;
}
.public-album-main { display: flex; flex-direction: column; color: inherit; }
.public-album-main:hover { text-decoration: none; }
.public-album-card:hover { border-color: var(--accent); }
.public-album-mosaic {
    display: grid; grid-template-columns: 2fr 1fr; gap: 2px; aspect-ratio: 3 / 2; background: var(--panel-2);
}
.public-album-tile { display: flex; align-items: center; justify-content: center; overflow: hidden; }
.public-album-tile.is-main { grid-row: 1 / span 2; }
.public-album-tile.is-placeholder { grid-column: 1 / -1; grid-row: 1 / span 2; color: var(--muted); }
.public-album-tile img { width: 100%; height: 100%; object-fit: cover; }
.public-album-tile-side { display: grid; grid-template-rows: 1fr 1fr; gap: 2px; overflow: hidden; }
.public-album-tile-side img { width: 100%; height: 100%; object-fit: cover; min-height: 0; }
.public-album-title { padding: 8px 10px 0; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.public-album-count { padding: 0 10px 6px; }
.public-album-chips { display: flex; flex-wrap: wrap; gap: 4px; padding: 0 10px 10px; }
@media (max-width: 759px) {
    .public-album-grid { grid-template-columns: 1fr; }
}

/* M32: the search bar. */
.gallery-search { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 16px; }
/* M62: when the search-autocomplete controller wraps the input in .ac-wrap, the wrapper becomes the
   flex child — carry the input's flex sizing onto it so the layout (and the Search button) stay put. */
.gallery-search .ac-wrap { flex: 1 1 260px; min-width: 0; }
.gallery-search-input {
    flex: 1 1 260px; min-width: 0; padding: 7px 12px;
    border: 1px solid var(--line); border-radius: 8px;
    color: var(--text); background: var(--panel); font-size: 14px;
}
.gallery-search-input:focus { outline: none; border-color: var(--accent); }
.gallery-search-btn {
    padding: 7px 14px; border: 1px solid var(--accent); border-radius: 8px;
    color: var(--bg); background: var(--accent); font-size: 13px; cursor: pointer;
}
.gallery-search-btn:hover { filter: brightness(1.08); }
/* V33 (M94 P1): the `🎲` random-draw submit button — a secondary (outline) button beside Search, so it
   reads as the same control group without competing with the primary Search fill. */
.gallery-random-btn {
    padding: 7px 12px; border: 1px solid var(--line); border-radius: 8px;
    color: var(--text); background: var(--panel); font-size: 15px; line-height: 1; cursor: pointer;
}
.gallery-random-btn:hover { border-color: var(--accent); }
/* Search precedes the 🎲 in the markup (it must be the form's first submit button, the Enter default);
   `order` draws Search and the clear link after the 🎲 again. */
.gallery-search > .gallery-search-btn, .gallery-search > .gallery-search-clear { order: 1; }
.gallery-search-clear { color: var(--muted); font-size: 13px; }
.gallery-search-clear:hover { color: var(--text); }

/* H4: the collapsible search-grammar help beneath the search box (native <details>). */
.search-help { margin: -8px 0 16px; font-size: 13px; }
.search-help-summary { color: var(--muted); cursor: pointer; width: fit-content; }
.search-help-summary:hover { color: var(--text); }
.search-help-list { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; margin: 10px 0 6px; padding: 12px 14px; background: var(--panel-2); border-radius: 8px; }
.search-help-list dt { white-space: nowrap; }
.search-help-list dt code { color: var(--accent); }
.search-help-list dd { margin: 0; color: var(--muted); }
.search-help-owner { color: var(--muted); font-size: 11px; }
.search-help-note { margin: 4px 0 0; color: var(--muted); font-size: 12px; }
@media (max-width: 560px) { .search-help-list { grid-template-columns: 1fr; gap: 2px 0; } .search-help-list dd { margin-bottom: 6px; } }
.search-help-debug { margin: 6px 0 0; font-size: 12px; }

/* version/9.0 X3: the query-debug page. Long SQL scrolls inside its own box, never the page (D4); the
   Audience radios wrap under the search box at a phone width. */
.search-debug-form { margin-bottom: 12px; }
.search-debug-audience { border: 0; padding: 0; margin: 0; display: inline-flex; flex-wrap: wrap; gap: 4px 12px; }
.search-debug-audience label, .search-debug-run { display: inline-flex; align-items: center; gap: 4px; color: var(--muted); font-size: 13px; }
.search-debug-section { margin: 20px 0; }
.search-debug-section h2 { font-size: 17px; margin: 0 0 8px; }
.search-debug-section h3 { font-size: 14px; margin: 12px 0 6px; }
.search-debug-canonical { overflow-wrap: anywhere; }
.search-debug-statement { margin: 10px 0; }
.search-debug-sql { max-width: 100%; overflow-x: auto; background: var(--panel-2); padding: 8px 10px; border-radius: 6px; font-size: 12px; line-height: 1.5; }
.search-debug-plan { font-size: 12px; }
.search-debug-plan td { overflow-wrap: anywhere; }

/* M43 / M94 P3: the /random page. V34 replaced the second search form with a one-line SUBSET line under
   the crumb; the Next control is the shell walk pill that greys out when the set has <= 1 item. */
.random .crumb { margin: 0 0 16px; font-size: 13px; }
/* V34: the subset line names the query (or the whole gallery) and the exact match count, and links back
   to the gallery to refine it. It wraps on a phone (D4). */
.random-subset { margin: 0 0 20px; font-size: 14px; color: var(--muted); }
.random-subset code { color: var(--text); }
.random-subset a { color: var(--accent); }
/* G2 (M72): the /random "next random" control reuses the shell walk's `.preview-gallery-nav` pill and
   `.gallery-nav-link` styling — no bespoke `.random-nav`/`.random-next` rules any more. */

/* V35/V36 (M94 P5): the share validity line — on the collection listing, the collection file page and
   the item file page. `🔗 Shared with you · … · valid for <remaining>`, the exact instant in the `time`
   hover title. Wraps to two rows on a phone (D4). */
.share-validity { margin: 0 0 18px; font-size: 14px; color: var(--muted); }
.share-validity time { color: var(--text); border-bottom: 1px dotted var(--muted); cursor: help; }

.facets-wrap { margin-bottom: 20px; }
.facets { display: flex; flex-wrap: wrap; gap: 8px; }
/* M49 G2: each facet is one segmented [+ tag −] chip. The group is the flex item; its three segments
   butt together into one tight pill. The +/- ends render hidden and JS reveals them AND marks the group
   `.is-segmented`, so with JS off the name segment is the whole pill and the only control. */
.facet-group { display: inline-flex; align-items: stretch; }
.facet-seg--add, .facet-seg--exclude {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 24px; padding: 0 6px;
    border: 1px solid var(--line); border-radius: 999px;
    color: var(--muted); background: var(--panel); font-size: 14px; line-height: 1; cursor: pointer;
}
.facet-seg--add:hover { border-color: #3f7a4f; color: #8fce9f; }
.facet-seg--exclude:hover { border-color: #7a3f3f; color: #e58b8b; }
.facet-seg--add[aria-pressed="true"] { color: #8fce9f; border-color: #3f7a4f; background: var(--panel-2); }
.facet-seg--exclude[aria-pressed="true"] { color: #e58b8b; border-color: #7a3f3f; background: var(--panel-2); }
/* Once JS reveals the ends, collapse the shared borders so the three read as ONE tight pill: the `+`
   rounds the left, the name goes square, the `−` rounds the right. Without JS the name keeps `.facet`'s
   full 999px pill. */
.facet-group.is-segmented .facet-seg--add { border-radius: 999px 0 0 999px; border-right: none; }
.facet-group.is-segmented .facet-seg--exclude { border-radius: 0 999px 999px 0; border-left: none; }
.facet-group.is-segmented .facet-seg--name { border-radius: 0; }
/* M23: clamp the tag bar to ~3 rows; the facets controller adds this class in JS (so a no-JS visitor
   keeps the full bar) and the toggle reveals the rest. Not -webkit-line-clamp — that has no effect on
   a flex-wrap chip row; a max-height + overflow does. */
/* M94 P2: the clamp height is a custom property the controller MEASURES (3 chip rows + 2 row-gaps from
   the live bar) and re-measures on resize, so a phone's taller 40px chips still show exactly three rows.
   The fallback keeps the old desktop-chip estimate for the no-JS / pre-measure frame. The class toggle
   stays the only open/close switch — the property never becomes an inline max-height. */
.facets--clamped { max-height: var(--facets-clamp, calc(3 * 28px + 2 * 8px)); overflow: hidden; }
.facet {
    padding: 4px 10px; border: 1px solid var(--line); border-radius: 999px;
    color: var(--muted); background: var(--panel); font-size: 13px;
}
.facet.is-active { color: var(--text); border-color: var(--accent); }
.facet-count { color: var(--muted); font-size: 11px; }
.facets-toggle {
    margin-top: 8px; padding: 2px 8px; border: 1px solid var(--line); border-radius: 999px;
    color: var(--muted); background: var(--panel); font-size: 12px; cursor: pointer;
}
.facets-toggle:hover { color: var(--text); border-color: var(--accent); }

/* M32 S4 / M44: the per-page size control. The `.page-size-label` and `.page-size-opt` chips are reused
   by the M44 `.pagination-size` column; the old standalone `.page-size` container is gone. */
.page-size-label { color: var(--muted); font-size: 12px; }
.page-size-opt {
    padding: 2px 9px; border: 1px solid var(--line); border-radius: 999px;
    color: var(--muted); background: var(--panel); font-size: 12px;
}
.page-size-opt:hover { color: var(--text); border-color: var(--accent); text-decoration: none; }
.page-size-opt.is-current { color: var(--text); border-color: var(--accent); }

.card-grid {
    display: grid; gap: 14px;
    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}

.card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
/* M71: the `locate:<ulid>` redirect ends in `#item-<ulid>`; the native fragment scrolls the card into
   view (offset by scroll-margin so a sticky header does not cover it) and :target highlights it. */
.card { scroll-margin-top: 80px; }
.card:target {
    outline: 2px solid var(--accent, #4c8dff);
    outline-offset: 2px;
    box-shadow: 0 0 0 4px rgba(76, 141, 255, .25);
}
@media (prefers-reduced-motion: no-preference) {
    .card:target { animation: card-locate-pulse 1.4s ease-out 1; }
}
@keyframes card-locate-pulse {
    0% { box-shadow: 0 0 0 6px rgba(76, 141, 255, .45); }
    100% { box-shadow: 0 0 0 4px rgba(76, 141, 255, .25); }
}
.card-link { display: block; color: inherit; }
.card-link:hover { text-decoration: none; }
.card-thumb {
    position: relative;
    aspect-ratio: 1 / 1; display: flex; align-items: center; justify-content: center;
    background: var(--panel-2); overflow: hidden;
}
.card-thumb img { width: 100%; height: 100%; object-fit: cover; }
/* Hover-to-play moving preview (M11 clip/GIF): layered over the still, revealed on hover. */
.card-motion {
    position: absolute; inset: 0;
    width: 100%; height: 100%; object-fit: cover;
    opacity: 0; transition: opacity .12s ease;
    pointer-events: none;
}
.card-thumb.is-playing .card-motion { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
    .card-motion { transition: none; }
}
.type-icon { color: var(--muted); display: inline-flex; }
.type-icon .type-glyph { width: 44px; height: 44px; }

/* Small type badge pinned to the top-left corner over the thumbnail. */
.card-badge {
    position: absolute; top: 6px; left: 6px; z-index: 1;
    padding: 2px 7px; border-radius: 999px;
    font-size: 11px; font-weight: 600; line-height: 1.45; letter-spacing: .02em;
    color: #fff; background: rgba(0, 0, 0, .66);
    border: 1px solid rgba(255, 255, 255, .18);
    backdrop-filter: blur(2px); pointer-events: none; text-transform: uppercase;
}
/* V33/V37 (M94 P2): nine kind-badge COLOUR FAMILIES, one class per family (never one per token). A plain
   still image and a static WebP show NO badge at all (badgeFamily null), so there is no image family. The
   text stays the lowercase MediaKind::badge() token uppercased by CSS above (P-D1). Shared with the
   preview title-row badge below. Do NOT reintroduce a per-token class (`--gif`, `--exec`, …). */
.card-badge--video      { background: rgba(52, 88, 120, .82); }   /* blue */
.card-badge--animated   { background: rgba(120, 74, 40, .82); }   /* orange */
.card-badge--audio      { background: rgba(84, 66, 120, .82); }   /* violet */
.card-badge--pdf        { background: rgba(120, 40, 52, .82); }   /* red */
.card-badge--text       { background: rgba(40, 100, 100, .82); }  /* teal */
.card-badge--archive    { background: rgba(120, 96, 40, .82); }   /* amber */
.card-badge--executable { background: rgba(96, 30, 30, .85); }    /* dark red */
.card-badge--design     { background: rgba(112, 44, 104, .82); }  /* magenta */
.card-badge--document   { background: rgba(40, 92, 64, .82); }    /* green — office docs & e-books (D2) */
.card-badge--other      { background: rgba(70, 74, 82, .82); }    /* grey */
.card-name {
    padding: 8px 10px; font-size: 13px; color: var(--text);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.empty { color: var(--muted); padding: 40px 0; }

.pager {
    display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: center;
    margin-top: 28px;
}
.pager-num {
    min-width: 34px; padding: 6px 10px; border: 1px solid var(--line); border-radius: 8px;
    color: var(--muted); background: var(--panel); font-size: 13px; text-align: center;
}
a.pager-num:hover { color: var(--text); border-color: var(--accent); }
.pager-num.is-current {
    color: var(--text); background: var(--panel-2); border-color: var(--accent); font-weight: 600;
}
.pager-edge {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 34px; padding: 6px 8px; border: 1px solid var(--line); border-radius: 8px;
    color: var(--muted); background: var(--panel); font-size: 15px;
}
a.pager-edge:hover { color: var(--text); border-color: var(--accent); }
.pager-edge.is-disabled { opacity: 0.4; cursor: default; }
.pager-gap { color: var(--muted); font-size: 13px; padding: 0 2px; }
.pager-pos { color: var(--muted); font-size: 13px; margin-left: 4px; }

/* M88 D8: the phone compact pager — `‹ Page N of M ›` with a tap-to-jump GET form (no script). Hidden on
   desktop; the breakpoint block below swaps it in for the numbered `.pager` at <= 759px. */
.pager-compact { display: none; gap: 10px; align-items: center; justify-content: center; margin-top: 28px; }
.pager-jump { position: relative; }
.pager-jump > summary { list-style: none; cursor: pointer; color: var(--muted); font-size: 14px; padding: 6px 8px; }
.pager-jump > summary::-webkit-details-marker { display: none; }
.pager-jump[open] > summary { color: var(--text); }
.pager-jump form {
    position: absolute; left: 50%; transform: translateX(-50%); margin-top: 6px; z-index: 5;
    display: flex; gap: 6px; align-items: center; padding: 8px; white-space: nowrap;
    background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px;
}
.pager-jump form input[type="number"] { width: 5em; padding: 6px 8px; background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 6px; }
.pager-jump form button { padding: 6px 12px; border: 1px solid var(--accent); border-radius: 6px; background: var(--accent); color: var(--bg); cursor: pointer; }

/* M44: the standardized two-column pagination control — the pager on the left (~70%), the per-page
   size selector on the right (~30%). CSS Grid to match the codebase; it stacks to one column on a
   narrow viewport. The `pagination` Stimulus controller trims the pager to fit the left column. */
.pagination {
    display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 3fr);
    gap: 10px 20px; align-items: center; margin-top: 28px;
}
.pagination-pager { min-width: 0; }
/* The pager already carries its own top margin; inside the grid the row owns the spacing. */
.pagination-pager .pager { margin-top: 0; }
.pagination-size {
    display: flex; flex-wrap: wrap; align-items: center; gap: 8px; justify-content: flex-end;
}
@media (max-width: 759px) {
    .pagination { grid-template-columns: 1fr; }
    .pagination-size { justify-content: center; }
    /* M88 D8: on a phone the numbered pager gives way to the compact `‹ Page N of M ›` row. */
    .pagination-pager .pager { display: none; }
    .pager-compact { display: flex; }
    /* M49 G3: on a phone the [+ tag −] chip splits into three full-size touch targets — no "fishing" for
       a tiny +/−. The group stays align-items:stretch, so the ends match the taller name segment's
       height (≈40px from the name padding). Shared across the public and admin bars. Only applies once
       JS has revealed the ends (`.is-segmented`); the desktop rules above stay tight above 640px. */
    .facet-group.is-segmented .facet-seg--add,
    .facet-group.is-segmented .facet-seg--exclude { min-width: 40px; padding: 0 12px; font-size: 18px; }
    .facet-group.is-segmented .facet-seg--name { padding-top: 10px; padding-bottom: 10px; }
    .facets { gap: 10px; }
    /* V33 (M94 P1), D4: on a phone the search group wraps — the input takes one full row, then `🎲` and
       Search share the next row as full-height (>= 40px) touch targets. */
    .gallery-search-input, .gallery-search .ac-wrap { flex-basis: 100%; }
    .gallery-random-btn, .gallery-search-btn { flex: 1 1 auto; min-height: 40px; }
}

/* ---- archive tree ------------------------------------------------------- */
.crumb { margin: 0 0 12px; font-size: 13px; }
.archive-sub { color: var(--muted); margin: 4px 0 16px; font-size: 13px; }
.download-btn {
    display: inline-block; padding: 8px 16px; border-radius: 8px;
    background: var(--accent); color: #0b1020; font-weight: 600;
}
.download-btn:hover { text-decoration: none; filter: brightness(1.05); }

.tree { list-style: none; margin: 24px 0 8px; padding: 0; border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
.tree-row {
    display: flex; justify-content: space-between; gap: 12px;
    padding: 6px 14px; padding-left: calc(14px + var(--depth, 0) * 18px);
    border-top: 1px solid var(--line);
}
.tree-row:first-child { border-top: none; }
.tree-row.is-dir { color: var(--muted); }
.tree-size { color: var(--muted); font-variant-numeric: tabular-nums; font-size: 12px; }
.tree-note { color: var(--muted); font-size: 12px; }

/* ---- admin chrome (M88 S3: two-row header + phone menu) ------------------ */
/* The admin width is one custom property (C-admin-width); M96 G2 overrides it in desktop mode. */
.admin-shell { --admin-width: 1100px; }
.admin-header { border-bottom: 1px solid var(--line); }
.admin-row { max-width: var(--admin-width); margin: 0 auto; padding: 14px 20px; }
.admin-row-1 { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
.admin-sections { display: flex; gap: 16px; flex-wrap: wrap; }
.admin-sections a { color: var(--muted); }
/* Immediate hover affordance on a row-1 section link, so the owner sees which menu item the pointer is on
   (matches .admin-subnav a:hover). The row-2 preview and its underline still follow after the 80 ms rest. */
.admin-sections a:hover { color: var(--text); }
.admin-sections a.is-active { color: var(--text); font-weight: 600; }
/* The section whose row-2 list the admin-nav controller previews on hover/focus (M151, N-D5): a light
   underline so the owner sees which section the shown row-2 list belongs to. */
.admin-sections a.is-previewed { text-decoration: underline; text-decoration-color: var(--line); text-underline-offset: 3px; }
.nav-item { display: inline-flex; align-items: baseline; gap: 4px; }
.nav-badge {
    display: inline-flex; align-items: center; padding: 0 6px; border-radius: 999px;
    background: var(--panel-2); border: 1px solid var(--line); color: var(--muted); font-size: 11px;
}
main.admin { max-width: var(--admin-width); }

/* The phone hamburger: a native <details> overlay, hidden on desktop; the breakpoint block reveals it. */
.admin-menu { display: none; }
.admin-menu-toggle { list-style: none; cursor: pointer; font-size: 20px; line-height: 1; color: var(--text); }
.admin-menu-toggle::-webkit-details-marker { display: none; }
.admin-menu-panel {
    position: absolute; left: 0; right: 0; top: 100%; z-index: 20;
    background: var(--panel); border-bottom: 1px solid var(--line); padding: 12px 20px;
    max-height: calc(100vh - 56px); overflow: auto;
}
/* A closed menu's absolutely-positioned panel must not lay out (or it widens the document); only the
   open overlay renders. */
.admin-menu:not([open]) .admin-menu-panel { display: none; }
.menu-section { padding: 8px 0; border-bottom: 1px solid var(--line); }
.menu-section h3 { margin: 0 0 6px; font-size: 13px; color: var(--muted); font-weight: 700; }
.menu-section.is-active h3 { color: var(--text); }
.menu-pages { display: flex; flex-direction: column; gap: 6px; }
.menu-pages a { color: var(--muted); }
.menu-pages a.is-active { color: var(--text); font-weight: 600; }
.menu-foot { display: flex; align-items: center; gap: 16px; padding-top: 10px; }
/* The `hidden` attribute must always win, even over a component's own display rule (e.g. .share-out
   is inline-flex, which would otherwise reveal a `hidden` copy field before a link is minted). */
[hidden] { display: none !important; }

.admin-brand { color: var(--text); font-weight: 700; }
.admin-brand-ver { color: var(--muted); font-weight: 600; font-variant-numeric: tabular-nums; }
.admin-brand-instance { margin-left: 6px; padding: 1px 6px; border-radius: 4px; background: var(--surface-2, rgba(127,127,127,.16)); color: var(--muted); font-weight: 600; font-size: 12px; }
.item-global-ref { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 6px 0; }
.item-global-ref-value { font-size: 12px; word-break: break-all; padding: 2px 6px; border-radius: 4px; background: var(--surface-2, rgba(127,127,127,.12)); }
.admin-nav-right { margin-left: auto; display: flex; gap: 14px; font-size: 13px; }
.admin-logout-form { margin: 0; }

/* Row 2: the pages of the current section, on every admin page. One hairline under the header row. */
.admin-row-2.admin-subnav {
    display: flex; flex-wrap: wrap; gap: 16px; padding-top: 8px; padding-bottom: 8px;
    border-top: 1px solid var(--line); font-size: 13px;
}
.admin-subnav a { color: var(--muted); }
.admin-subnav a:hover { color: var(--text); }
.admin-subnav a.is-active { color: var(--text); font-weight: 600; }

/* Phone (D4): only the brand and ☰ show; the open menu overlays the content (no shift). */
@media (max-width: 759px) {
    .admin-row-1 { position: relative; }
    .admin-sections, .admin-row-2, .admin-nav-right { display: none; }
    .admin-menu { display: block; margin-left: auto; }
}

/* Dashboard queue KPI boxes (the liveness badge stays a separate line above). */
.worker-queues { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 20px; }
.worker-queue-item {
    display: inline-flex; align-items: baseline; gap: 6px; padding: 4px 11px;
    border: 1px solid var(--line); border-radius: 999px; background: var(--panel); font-size: 12px;
}
.worker-queue-label { color: var(--muted); }
.worker-queue-count { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
.queue-kpis { margin: 0 0 20px; }
.queue-kpis-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 8px; margin: 0 0 10px; }
.queue-kpis-head h2, .worker-health h2, .maintenance-box h2 { margin: 0; font-size: 16px; }
.queue-kpis-freshness { color: var(--muted); font-size: 12px; }
.queue-kpis-freshness.is-stale { color: var(--warn); }
.queue-kpis-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 10px; }
.queue-kpi { min-width: 0; padding: 12px 14px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); }
.queue-kpi--offline, .queue-kpi--unavailable { border-color: #7a6a2f; }
.queue-kpi-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.queue-kpi-head h3 { margin: 0; font-size: 14px; font-weight: 600; overflow-wrap: anywhere; }
.queue-kpi-head strong { color: var(--text); font-size: 20px; font-variant-numeric: tabular-nums; }
.queue-kpi-detail { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; margin: 12px 0 8px; }
.queue-kpi-detail div { min-width: 0; }
.queue-kpi-detail dt { color: var(--muted); font-size: 10px; text-transform: uppercase; }
.queue-kpi-detail dd { margin: 2px 0 0; font-variant-numeric: tabular-nums; }
.queue-kpi-state { margin: 0; color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
.worker-health-maintenance { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(220px, 1fr); gap: 12px; margin: 0 0 22px; }
.worker-health, .maintenance-box { padding: 12px 14px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); }
.worker-health-list { display: flex; flex-direction: column; gap: 7px; margin-top: 10px; }
.worker-health-row { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; font-size: 12px; }
.worker-health-row .worker-dot { margin-right: 1px; }
.maintenance-box p { margin: 10px 0 0; font-size: 13px; }
@media (max-width: 759px) {
    .worker-health-maintenance { grid-template-columns: 1fr; }
}
.admin-logout { padding: 0; border: 0; background: none; color: var(--muted); font: inherit; cursor: pointer; }

.admin-title { margin: 0 0 18px; font-size: 22px; }
.admin-lead { color: var(--muted); margin: 0 0 20px; }
.admin-lead code, code { background: var(--panel-2); padding: 1px 5px; border-radius: 4px; font-size: 13px; }

/* Shared app-wide flash region: a fixed top-right toast stack (templates/_flashes.html.twig). */
.flash-region {
    position: fixed; top: 16px; right: 16px; z-index: 1000;
    display: flex; flex-direction: column; gap: 10px;
    width: min(420px, calc(100vw - 32px)); pointer-events: none;
}
.flash-region:empty { display: none; }
.flash {
    display: flex; align-items: flex-start; gap: 10px;
    padding: 10px 14px; border-radius: 8px; border: 1px solid var(--line);
    background: var(--panel); box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
    pointer-events: auto;
    opacity: 1; transform: translateX(0); transition: opacity 0.25s ease, transform 0.25s ease;
}
.flash--leaving { opacity: 0; transform: translateX(12px); }
.flash-msg { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.flash-close {
    flex: none; padding: 2px; margin: -2px -4px 0 0; border: 0; background: none;
    color: var(--muted); cursor: pointer; line-height: 0;
}
.flash-close:hover { color: var(--text); }
.flash-success { border-color: var(--ok); background: var(--ok-bg); }
.flash-error { border-color: var(--warn); background: var(--warn-bg); }
.flash-info { border-color: var(--info); background: var(--info-bg); }
.flash-warning { border-color: var(--warn); background: var(--warn-bg); }
/* M88 S4: on a phone the region is full width at the top. */
@media (max-width: 759px) {
    .flash-region { top: 8px; left: 8px; right: 8px; width: auto; }
}
.copy-field { width: 100%; margin-top: 8px; padding: 6px 8px; background: var(--panel-2); color: var(--text); border: 1px solid var(--line); border-radius: 6px; }
/* M89 L3 (V0b): the album-id copy field sits inline in the items crumb — a compact, click-to-select
   box, not the full-width default. */
.copy-album-id { width: 15em; max-width: 60vw; margin: 0 4px; padding: 1px 6px; vertical-align: middle; }

/* stat grid */
.stat-grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); margin-bottom: 24px; }
.stat { display: flex; flex-direction: column; gap: 6px; padding: 16px; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; color: inherit; }
.stat:hover { text-decoration: none; border-color: var(--accent); }
.stat-num { font-size: 28px; font-weight: 700; }
.stat-label { color: var(--muted); font-size: 13px; }
.stat-warn .stat-num { color: #e5896b; }
/* One shared grid keeps queue and library cards equal, including wrapped/mobile rows. */
.dashboard-kpis-grid { grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr)); grid-auto-rows: 1fr; gap: 10px; margin-bottom: 0; }
.dashboard-kpi { display: flex; flex-direction: column; gap: 0; min-height: 190px; box-sizing: border-box; padding: 14px; }
.dashboard-kpi .queue-kpi-head strong { font-size: 28px; }
.dashboard-kpi .stat-label { margin-top: 6px; }
.dashboard-kpi-refreshed { margin: auto 0 0; padding-top: 12px; color: var(--muted); font-size: 11px; }
.dashboard-kpi-refreshed.is-stale { color: var(--warn); }
.dashboard-kpi-note, .queue-kpi-fallback { margin: 8px 0 0; color: var(--muted); font-size: 11px; }


.admin-actions { display: flex; gap: 12px; flex-wrap: wrap; }

/* buttons + forms */
.btn { display: inline-block; padding: 6px 12px; border-radius: 7px; border: 1px solid var(--line); background: var(--panel-2); color: var(--text); font: inherit; font-size: 13px; cursor: pointer; }
.btn:hover { text-decoration: none; border-color: var(--accent); }
.btn-primary { background: var(--accent); color: #0b1020; border-color: var(--accent); font-weight: 600; }
.btn-danger { border-color: #7a2f2f; }
.btn-danger:hover { border-color: #c0524f; }
/* A disabled button reads disabled: dimmed, not-clickable, and no hover affordance. Covers the sibling
   review's settle gate (Keep 1 / Keep 2 blocked for the first second) and an illegal "Album both". */
.btn:disabled, .btn[disabled] { opacity: 0.45; cursor: not-allowed; }
.btn:disabled:hover, .btn[disabled]:hover { border-color: var(--line); text-decoration: none; }

/* The one inline confirm (M88, D10): an in-page destructive-alert panel the `inline-confirm`
   controller reveals in place of a browser dialog. No `hidden` in the markup, so it works with JS off. */
.inline-confirm { margin: 8px 0; padding: 10px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); }
.inline-confirm--danger { border-color: #7a2f2f; background: #2a1616; }
.inline-confirm__message { margin: 0 0 8px; color: var(--text); }
.inline-confirm__actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }

/* No max-width on .field (owner rule): a field fills its container. Do not re-add a width cap. */
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
.field span { color: var(--muted); font-size: 13px; }
.field-check { flex-direction: row; align-items: center; gap: 8px; }
.field-check input { width: auto; }
.field input, .field textarea, .generate-form select, .generate-form input, .inline-form input, .ban-note-input {
    padding: 8px 10px; background: var(--panel-2); color: var(--text);
    border: 1px solid var(--line); border-radius: 7px; font: inherit;
}

/* version/9.3 M147 (ban-note-input N4): the comma-aware ban-reason field. The input carries the admin
   control tokens above; the controller wraps it in .ac-wrap and anchors the shared .ac-menu dropdown. */
.ban-note { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; }
.ban-note > label { color: var(--muted); font-size: 13px; }
.ban-note-input { width: 100%; }
.ban-note-help, .ban-note-recent-help { margin: 0; }
.ban-reason-filter { align-items: flex-end; gap: 8px; }
.ban-reason-filter .ac-wrap, .ban-reason-filter .ban-note-input { flex: 1 1 260px; min-width: 0; }
.form-actions { display: flex; gap: 12px; align-items: center; margin-top: 8px; }
.inline-form { display: inline-flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 12px; }
.check { color: var(--muted); font-size: 13px; display: inline-flex; gap: 6px; align-items: center; }

.generate-form { display: flex; gap: 18px; flex-wrap: wrap; align-items: flex-end; padding: 16px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); margin-bottom: 24px; }
.generate-form fieldset { border: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 8px; }
.generate-form legend { color: var(--muted); font-size: 13px; padding: 0; }
.radio { display: inline-flex; gap: 6px; align-items: center; font-size: 14px; }

/* tables */
.admin-table { width: 100%; border-collapse: collapse; margin: 8px 0 24px; }
.admin-table th, .admin-table td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
.admin-table th { color: var(--muted); font-weight: 600; font-size: 13px; }
.admin-table tr.is-restricted td:first-child { box-shadow: inset 3px 0 0 #c0524f; }
/* M36: a hidden tag row gets a muted left bar. Scoped :not(.is-restricted) so a both-flags row keeps
   the restricted bar — restricted dominates, hidden is the second marker (the separate hidden badge). */
.admin-table tr.is-hidden:not(.is-restricted) td:first-child { box-shadow: inset 3px 0 0 var(--muted); }
/* Force long, space-less filenames to wrap inside the name column instead of blowing out the table. */
.admin-table td.col-name { max-width: 420px; overflow-wrap: anywhere; word-break: break-word; }
/* Tags page: keep every cell a normal table-cell (never display:flex, which drops a <td> out of the
   row model and misaligns its bottom border) and strip the in-cell form margin so each row's divider
   lines up across Name / Items / Access / action. */
.tags-table td { vertical-align: middle; }
.tags-table td form { margin: 0; }
.admin-table tr.is-disabled { opacity: 0.55; }
/* Actions sit in their own full-width row under each item: a single horizontal cluster, no column. */
.row-actions { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
/* Every direct action reads as the same control: tree (link), edit, share, delete are all .btn. */
.row-actions > a.btn, .row-actions > .edit-toggle, .row-actions form > .btn { display: inline-flex; align-items: center; gap: 4px; }
.row-actions form { margin: 0; }
/* The actions row is glued to its item above (no top border, tight top padding) and indented to line
   up under the name column, so it reads as belonging to that item rather than as a new record. */
/* An item spans a content row + an actions row (+ an optional edit row); the content row drops its
   bottom border so the pair reads as one block, and the actions/edit row below carries the divider. */
.admin-table tr.item-row > td { border-bottom: 0; }
.admin-table tr.actions-row > td { border-top: 0; padding: 2px 8px 12px 116px; }
@media (max-width: 640px) { .admin-table tr.actions-row > td { padding-left: 8px; } }

/* M88 S8 (D4, V3/V4): the phone table-to-cards collapse — OPT-IN via `admin-table--cards`
   (Q-table-cards-selector, option B). A table WITHOUT the class keeps its columns (inside a
   `.table-scroll` overflow box if needed). Each consumer adds the class together with `data-label`
   attributes on every td; a `data-label=""` cell spans full width with no label. S8 ships the mechanism
   only — M88 adds the class to no page table. */
.table-scroll { overflow-x: auto; }
@media (max-width: 759px) {
    table.admin-table.admin-table--cards,
    table.admin-table.admin-table--cards tbody,
    table.admin-table.admin-table--cards tr,
    table.admin-table.admin-table--cards td { display: block; width: auto; }
    /* Keep the header for screen readers, hide it visually. */
    table.admin-table.admin-table--cards thead {
        position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
        overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
    }
    table.admin-table.admin-table--cards tr {
        margin-bottom: 12px; padding: 10px 14px; border: 1px solid var(--line);
        border-radius: 8px; background: var(--panel);
    }
    table.admin-table.admin-table--cards td {
        display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 4px 10px;
        padding: 4px 0; border-bottom: 0;
    }
    table.admin-table.admin-table--cards td::before {
        content: attr(data-label); color: var(--muted); font-size: 12px;
    }
    /* A label-less cell (thumbnail, select checkbox, name headline) spans the full card width. */
    table.admin-table.admin-table--cards td[data-label=""] { display: block; }
    table.admin-table.admin-table--cards td[data-label=""]::before { content: none; }
    /* The right-aligned actions cell of V4 becomes left-aligned and wraps under the fields. */
    table.admin-table.admin-table--cards td.col-actions { display: block; text-align: left; }
    /* An inline expansion row (edit panel, re-point row, inline confirm) spans the card width. */
    table.admin-table.admin-table--cards tr.row-expansion { display: block; }
}
.muted { color: var(--muted); }
.small { font-size: 12px; }
.ingest-error { color: #e5896b; font-size: 12px; margin-top: 4px; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.status-age { margin-top: 4px; }

/* worker/queue liveness strip (M-ingest-status) */
.worker-status { display: inline-flex; align-items: center; gap: 8px; margin: 0 0 16px; padding: 5px 12px;
    border: 1px solid var(--line); border-radius: 999px; font-size: 12px; color: var(--muted); }
.worker-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex: none; }
.worker-dot.is-alive { background: #7fd39b; }
.worker-dot.is-stale { background: #d3bd7f; }
.worker-dot.is-offline { background: #e58b8b; }
.worker-sep { opacity: 0.5; }

/* admin blob list */
.table-scroll { overflow-x: auto; }
.stat-grid--compact { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; }
.cell-thumb { width: 56px; }
.blob-thumb { width: 48px; height: 48px; object-fit: cover; border-radius: 6px; background: var(--panel-2); display: block; }
.copy-sha { width: 130px; max-width: 40vw; margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; }

.badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; border: 1px solid var(--line); color: var(--muted); }
.badge-ready, .badge-public { border-color: #2f6b3f; color: #7fd39b; }
.badge-pending { border-color: #6b5a2f; color: #d3bd7f; }
.badge-failed { border-color: #7a2f2f; color: #e58b8b; }
/* M73: a banned-content rejection is its own terminal status badge — a warm amber-red distinct from the
   plain failed red, so the owner tells a ban apart from any other failure at a glance. */
.badge-banned_content { border-color: #7a4a2f; color: #e5b08b; }
.badge-restricted { border-color: #7a2f2f; color: #e58b8b; }
/* M36: the hidden badge shares the hidden muted token — a dashed border sets it apart from the plain
   muted .badge above (:276), which is solid. */
.badge-hidden { border-color: var(--line); border-style: dashed; color: var(--muted); }
.badge-expanded, .badge-awaiting_upload { border-color: var(--line); color: var(--muted); }
/* M18b import-by-URL: an item awaiting its server-side fetch. In-flight, like pending. */
.badge-awaiting_fetch { border-color: #6b5a2f; color: #d3bd7f; }
/* M20a: content-shared (dedup) marker — an item whose blob is referenced by another item. */
.badge-shared { border-color: #3a5a7a; color: #8fbfe0; display: inline-flex; align-items: center; gap: 4px; }
/* NP4 ban propagation: owner-only markers on items whose content is banned. "banned" is a direct ban
   (the strong, dangerous case: live items on banned bytes); "banned by proxy" is the weaker flag. */
.badge-banned { border-color: #7a2f2f; color: #e58b8b; display: inline-flex; align-items: center; gap: 4px; }
.badge-proxy-banned { border-color: #6b5a2f; color: #d3bd7f; display: inline-flex; align-items: center; gap: 4px; }
.tag-chip { display: inline-block; padding: 1px 8px; margin: 0 4px 4px 0; border-radius: 999px; background: var(--panel-2); font-size: 12px; }

.edit-pop summary { cursor: pointer; color: var(--accent); font-size: 13px; }
.edit-form { margin-top: 10px; padding: 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); }

/* editor-page-styling K1 (version/9.4.5): the owner Edit panel (/admin/edit/{item}). .edit-form gives
   the card chrome; .edit-panel stacks the operation groups in a column, each fieldset a bordered panel,
   and the Apply/Cancel row reuses .form-actions + .btn. No width cap on a field (owner UI rule); text
   fields grow and shrink so the page has no horizontal scroll at phone width. */
.edit-panel { display: flex; flex-direction: column; gap: 14px; }
.edit-panel fieldset { border: 1px solid var(--line); border-radius: 8px; margin: 0; padding: 12px 14px; display: flex; flex-direction: column; gap: 10px; }
.edit-panel legend { padding: 0; color: var(--muted); font-size: 13px; font-weight: 600; }
.edit-panel label { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; color: var(--text); font-size: 14px; }
.edit-panel input[type="number"], .edit-panel input[type="text"], .edit-panel select {
    padding: 8px 10px; background: var(--panel-2); color: var(--text);
    border: 1px solid var(--line); border-radius: 7px; font: inherit;
}
.edit-panel input[type="number"] { width: 8em; }
.edit-panel input[type="text"] { flex: 1 1 12em; min-width: 0; }

.preview { margin-bottom: 24px; }
.preview details { border: 1px solid var(--line); border-radius: 8px; padding: 8px 12px; margin-bottom: 8px; background: var(--panel); }
.preview summary { cursor: pointer; }
.preview-list { margin: 8px 0 0; padding-left: 20px; }

/* upload: Upload / Extract mode toggle — one full-width row split 50/50 */
.mode-toggle { display: grid; grid-template-columns: 1fr 1fr; margin-bottom: 18px; border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
.mode-tab { font: inherit; font-weight: 600; padding: 10px 14px; background: var(--panel); color: var(--muted); border: 0; cursor: pointer; transition: background .12s ease, color .12s ease; }
.mode-tab + .mode-tab { border-left: 1px solid var(--line); }
.mode-tab:hover { color: var(--text); }
.mode-tab.is-active { background: var(--accent); color: #fff; }

/* upload: drop row — 70% dropzone / 30% paste, side by side */
.upload-drop-row { display: grid; grid-template-columns: 7fr 3fr; gap: 12px; margin-bottom: 18px; }
.upload-drop-row .dropzone { margin-bottom: 0; height: 100%; display: flex; align-items: center; justify-content: center; }
.paste-btn { width: 100%; height: 100%; }

/* upload dropzone + progress */
.dropzone {
    border: 2px dashed var(--line); border-radius: 12px; padding: 48px 20px;
    text-align: center; color: var(--muted); cursor: pointer; background: var(--panel);
    margin-bottom: 18px;
}
.dropzone.is-dragging { border-color: var(--accent); color: var(--text); }

/* M163 quick upload: the site-wide drag feedback. Shown while a file is dragged over a shell page (not
   the upload page); it says a drop opens Upload. No inline styles, so it renders under the preview CSP
   (style-src 'self' 'unsafe-inline'). */
.quick-upload-overlay {
    position: fixed; inset: 0; z-index: 9999; pointer-events: none;
    display: flex; align-items: center; justify-content: center;
    font-size: 20px; font-weight: 600; letter-spacing: .02em;
    color: var(--text); background: rgba(0, 0, 0, .45);
    border: 3px dashed var(--accent); border-radius: 0;
}

/* upload: file list — header and rows share ONE column grid so the columns line up.
   Columns: #  name  hash  size  status  progress  (cancel/retry) */
.up-list-head, .up-row { display: grid; grid-template-columns: 1.75rem minmax(0, 1fr) 7.5rem 4.5rem 5.5rem minmax(6rem, 10rem) auto; gap: 12px; align-items: center; }
.up-list-head { padding: 4px 12px 6px; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.up-notice { margin: 0 0 10px; padding: 8px 12px; border: 1px solid #7a6a2f; border-radius: 8px; background: rgba(122, 106, 47, .18); color: #e6d59a; font-size: 13px; }
/* M42 public-view page: the owner-only banner naming the forced-anonymous view; on the .up-notice pattern. */
.gallery-public-notice { margin: 0 0 14px; padding: 8px 12px; border: 1px solid #7a6a2f; border-radius: 8px; background: rgba(122, 106, 47, .18); color: #e6d59a; font-size: 13px; }
.gallery-public-notice a { margin-left: 6px; }
.up-list { display: flex; flex-direction: column; gap: 8px; }
.up-row { padding: 8px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); }
.up-num { color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
.up-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.up-shared { display: inline-block; padding: 0 6px; border-radius: 999px; border: 1px solid var(--line); background: var(--panel-2); color: var(--muted); font-size: 10px; text-transform: uppercase; letter-spacing: .04em; vertical-align: middle; }
.up-hash { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.up-size { text-align: right; font-variant-numeric: tabular-nums; color: var(--muted); font-size: 12px; }
/* the progress bar visual applies to ROWS only — in the header, .up-bar is just the "progress" label */
.up-row .up-bar { height: 8px; background: var(--panel-2); border-radius: 999px; overflow: hidden; }
.up-fill { display: block; height: 100%; background: var(--accent); width: 0; transition: width .15s ease; }
.up-status { font-size: 12px; color: var(--muted); }
.up-row[data-state="done"] .up-status { color: #7fd39b; }
.up-row[data-state="error"] { border-color: #7a2f2f; }
.up-row[data-state="error"] .up-status { color: #e58b8b; }
.up-retry { font: inherit; font-size: 12px; padding: 3px 8px; border-radius: 6px; border: 1px solid var(--line); background: var(--panel-2); color: var(--text); cursor: pointer; }
/* M89 L2: the per-row Remove control (and today's Cancel) sit in the trailing auto column. */
.up-row .up-remove, .up-row .up-cancel { justify-self: end; padding: 2px 8px; font-size: 12px; background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px; color: var(--text); cursor: pointer; }
.up-row .up-remove:hover, .up-row .up-cancel:hover { border-color: var(--warn); }

/* M98 R3 (D4): the shared 7-column grid needs ~476px of fixed tracks, so it scrolls sideways on a
   phone. Below the phone width the header and rows become a flex-wrap line: the name grows, the hash
   truncates, and the progress bar drops to its own full-width row — nothing forces horizontal scroll. */
@media (max-width: 759px) {
    .up-list-head, .up-row { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; }
    .up-list-head .up-name, .up-row .up-name { flex: 1 1 40%; }
    .up-hash { min-width: 0; }
    .up-row .up-bar { flex: 1 1 100%; }
    .up-row .up-remove, .up-row .up-cancel { justify-self: auto; margin-left: auto; }
}

/* login */
.login { max-width: 360px; margin: 80px auto; }
.login h1 { font-size: 22px; margin-bottom: 20px; }
.login-form { display: flex; flex-direction: column; gap: 14px; }
.login-form label { display: flex; flex-direction: column; gap: 6px; }
.login-form span { color: var(--muted); font-size: 13px; }
.login-form input { padding: 9px 11px; background: var(--panel-2); color: var(--text); border: 1px solid var(--line); border-radius: 7px; font: inherit; }
.login-form button { padding: 10px; border-radius: 7px; border: none; background: var(--accent); color: #0b1020; font-weight: 600; cursor: pointer; }
/* V32 (M88 S7): the remember-me checkbox is a row, and the ← gallery link sits under the card. */
.login-form label.field-check { flex-direction: row; align-items: center; gap: 8px; }
.login-form label.field-check input { width: auto; }
.login-back { display: inline-block; margin-top: 14px; color: var(--muted); font-size: 13px; }

/* ---- M12 icons (inline SVG glyphs share currentColor) -------------------- */
.act-ico, .badge-ico, .meta-ico { width: 1em; height: 1em; vertical-align: -0.15em; }
.meta-ico { color: var(--muted); }

/* ---- M12 admin gallery cards -------------------------------------------- */
.admin-card { display: flex; flex-direction: column; position: relative; }
.admin-card.is-restricted { border-color: #7a2f2f; }
/* M48 Ctrl-drag batch selection: a transient per-batch highlight painted onto each card the sweep
   touches (drag_select_controller.js), cleared on disengage. The durable selection is the checkbox. */
.admin-card.is-drag-target { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }
.card-select { position: absolute; top: 6px; right: 6px; z-index: 2; background: rgba(0,0,0,.55); border-radius: 6px; padding: 3px 5px; line-height: 0; }
/* M26 L4 corner badge cluster: a bottom-left flex row over the thumb (a single absolutely-positioned
   flex container, so the badges lay out side by side instead of stacking on top of each other). Shared
   by the public, owner, and admin card cores. */
.card-flags {
    position: absolute; bottom: 6px; left: 6px; z-index: 1;
    display: flex; gap: 4px; pointer-events: none;
}
.card-flag {
    display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; border-radius: 999px;
    color: #fff; background: rgba(0, 0, 0, .66);
    border: 1px solid rgba(255, 255, 255, .18);
    backdrop-filter: blur(2px); font-size: 12px; line-height: 0;
}
.card-flag--restricted { background: rgba(120, 40, 52, .85); }
.card-flag--album { background: rgba(52, 74, 120, .85); }
.card-flag--shared { background: rgba(40, 96, 110, .85); }
/* NP4: owner-only ban flag on a card (either kind — the title distinguishes direct vs proxy). */
.card-flag--banned { background: rgba(107, 90, 47, .9); }
/* tag-rules R-D24: the owner card rules flag — the file breaks a tag rule. */
.card-flag--rules { background: rgba(140, 80, 30, .9); }
/* M90 C1 (V11): owner-only near-duplicate ("sibling") link flag — a NON-LINK "≈N" pill (the flag cluster
   is inside the card link, so it can never be an <a>). It carries text, so it sizes to its content. */
.card-flag--nd-links { width: auto; min-width: 20px; padding: 0 6px; line-height: 18px; font-size: 11px;
    font-variant-numeric: tabular-nums; background: rgba(70, 60, 96, .88); }
/* M90 C1: the linking "≈ N siblings" badge on the non-card owner surfaces (file page, Items/Blobs tables,
   the sibling group head and the suggestion sides). */
.badge-nd-links { text-decoration: none; white-space: nowrap; }
.badge-nd-links:hover { border-color: var(--muted); color: var(--fg); }
.card-meta { display: flex; align-items: center; gap: 8px; padding: 8px 10px 0; }
.card-size { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; margin-left: auto; }
.card-tags { padding: 6px 10px 0; }
.card-lineage { margin: 6px 10px 0; font-size: 12px; color: var(--muted); }
.card-links { display: flex; flex-direction: column; gap: 6px; padding: 8px 10px 0; }
.card-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-start; padding: 10px; margin-top: auto; }

/* M36: hidden facet, declared BEFORE --restricted so a both-flags facet reads restricted (red fill +
   text + border-colour) with the hidden dashed border-style still visible. border-style is a channel
   .facet.is-active (:46) never sets, thus an active hidden facet keeps its active marker. A muted
   colour alone would be identical to the base .facet (:42-45 is already color: var(--muted)), so the
   dashed border-style is what sets it apart. NOT raised: at (0,1,0) after .facet it beats the base rule
   and deliberately loses color/border-color to .facet.is-active, which keeps the active marker. */
/* Hidden tags read as "muted-distinct" (M36), NOT red like restricted. The base .facet is already
   muted, so a dashed border alone was invisible — a panel-2 fill + a visible dashed border + italic
   set it apart from an ordinary facet without borrowing restricted's red. */
.facet--hidden { border-style: dashed; border-color: var(--muted); color: var(--muted); background: var(--panel-2); font-style: italic; }
.facet--hidden.is-active { border-color: var(--accent); color: var(--text); }
/* M36: restricted identity moves into background (the channel .facet.is-active never sets), beside the
   red border-colour/text it already carried, so an active restricted facet reads active AND restricted. */
.facet--restricted { border-color: #7a2f2f; color: #e58b8b; background: #2a1616; }

/* ---- M12 bulk action bar ------------------------------------------------ */
.bulk-bar {
    position: sticky; top: 0; z-index: 5;
    display: flex; flex-wrap: nowrap; gap: 12px; align-items: center;
    padding: 10px 14px; margin-bottom: 14px;
    border: 1px solid var(--line); border-radius: 10px; background: var(--panel);
}
/* One line: the label and the form's fixed controls keep their size; the free-text tag input alone
   absorbs the slack (min-width:0 lets it shrink below its content width instead of forcing a wrap). */
.bulk-all { flex: none; display: inline-flex; gap: 8px; align-items: center; color: var(--muted); font-size: 13px; white-space: nowrap; }
.bulk-form { flex: 1 1 auto; min-width: 0; display: flex; gap: 8px; align-items: center; flex-wrap: nowrap; margin: 0; }
.bulk-form select, .bulk-form .btn { flex: none; }
.bulk-form input[type="text"] { flex: 1 1 auto; min-width: 0; }
.bulk-form select, .bulk-form input {
    padding: 6px 9px; background: var(--panel-2); color: var(--text);
    border: 1px solid var(--line); border-radius: 7px; font: inherit; font-size: 13px;
}
/* bulk-return B-D7: the fast-mode checkbox sits beside Apply (album + siblings ops only). The box keeps
   its native look — the generic input rule above would box it — and the label reads at the bar's size. */
.bulk-fast { flex: none; display: inline-flex; gap: 6px; align-items: center; color: var(--muted); font-size: 13px; white-space: nowrap; }
.bulk-fast input[type="checkbox"] { flex: none; padding: 0; border: 0; border-radius: 0; background: none; width: auto; }
/* M98 R3 (D4): on a phone the single line cannot hold the label, the op select, the active field
   and Apply side by side (it forces ~634px of horizontal scroll), so the bar and the form wrap —
   the active field panel takes its own full-width row and the chip input shrinks inside it. */
@media (max-width: 759px) {
    .bulk-bar { flex-wrap: wrap; }
    .bulk-form { flex-wrap: wrap; }
    .bulk-form > .bulk-panel { flex: 1 1 100%; min-width: 0; }
    .bulk-form > select[name="op"] { flex: 1 1 auto; min-width: 0; }
    .bulk-form > button[type="submit"] { flex: 1 1 auto; }
}
/* Bulk-tag CHANGE page (/admin/tags/bulk) — distinct from the inline .bulk-form bar above. Two
   panels side by side (stacking on narrow screens), full-width fields inside, Preview button below. */
.bulk-change-form { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; margin: 0 0 16px; }
.bulk-change-form fieldset { border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; margin: 0; display: flex; flex-direction: column; gap: 12px; }
.bulk-change-form legend { color: var(--muted); font-size: 13px; padding: 0 4px; }
.bulk-change-form > label,
.bulk-change-form fieldset > label { display: flex; flex-direction: column; gap: 4px; color: var(--muted); font-size: 13px; }
.bulk-change-form select, .bulk-change-form input[type="text"] { width: 100%; padding: 6px 9px; background: var(--panel-2); color: var(--text); border: 1px solid var(--line); border-radius: 7px; font: inherit; font-size: 13px; }
.bulk-change-form .tag-chip-input-host { width: 100%; }
.bulk-change-form > .btn { grid-column: 1 / -1; justify-self: start; }
@media (max-width: 720px) { .bulk-change-form { grid-template-columns: 1fr; } }

/* M55 Y1 — mark tags by pattern (/admin/tags/mark): the three steps and the preview render as bordered
   panels, mirroring .bulk-change-form above so the page reads as the same admin form family. */
/* D6: full width, no max-width cap. */
.mark-pattern-form { display: flex; flex-direction: column; align-items: flex-start; gap: 16px; margin: 0 0 20px; }
.default-rules-builder, .default-rules-preview { border: 1px solid var(--line); border-radius: 10px; padding: 14px; margin: 0 0 18px; background: var(--panel); }
.default-rules-builder-head { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 12px 24px; margin-bottom: 12px; }
.default-rules-builder h2, .default-rules-preview h2 { margin: 0; font-size: 18px; }
.default-rules-form { display: flex; flex-direction: column; gap: 14px; }
.default-rule-fields { display: grid; grid-template-columns: minmax(180px, 2fr) minmax(150px, 1fr) minmax(150px, 1fr) auto; gap: 12px; align-items: end; }
.default-rule-fields .field { display: flex; flex-direction: column; gap: 5px; color: var(--muted); font-size: 13px; }
.default-rule-fields input, .default-rule-fields select { min-width: 0; padding: 7px 9px; background: var(--panel-2); color: var(--text); border: 1px solid var(--line); border-radius: 7px; font: inherit; }
.default-rule-fields .check { white-space: nowrap; }
.default-rules-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.default-rules-preview-names { columns: 3; margin: 10px 0; padding-left: 20px; }
.default-rules-confirm { margin: 12px 0 0; }
.tag-rules-table tr.is-editing { outline: 2px solid var(--accent); outline-offset: -2px; }
@media (max-width: 760px) { .default-rule-fields { grid-template-columns: 1fr; } .default-rules-preview-names { columns: 1; } }
.mark-pattern-form fieldset { align-self: stretch; border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; margin: 0; display: flex; flex-direction: column; gap: 10px; }
.mark-pattern-form legend { color: var(--muted); font-size: 13px; padding: 0 4px; }
.mark-pattern-form label { display: flex; flex-direction: column; gap: 4px; color: var(--muted); font-size: 13px; }
.mark-pattern-form label:has(input[type="radio"]), .mark-pattern-form label.mark-pattern-ignorecase { flex-direction: row; align-items: center; gap: 8px; color: var(--text); font-size: 14px; }
.mark-pattern-form input[type="text"] { width: 100%; padding: 6px 9px; background: var(--panel-2); color: var(--text); border: 1px solid var(--line); border-radius: 7px; font: inherit; font-size: 13px; }
.mark-pattern-preview { border: 1px solid var(--line); border-radius: 10px; background: var(--panel); padding: 16px; }
.mark-pattern-preview h2 { margin: 0 0 12px; font-size: 16px; }
.mark-pattern-stats { margin: 0 0 12px; padding-left: 18px; display: flex; flex-direction: column; gap: 4px; }
.mark-pattern-names { margin: 0 0 16px; }
.mark-pattern-names-box { display: flex; flex-wrap: wrap; gap: 6px; max-height: 260px; overflow-y: auto; margin-top: 8px; }
.mark-pattern-confirm { margin: 0; }

.bulk-result { border: 1px solid #2f6b3f; border-radius: 10px; background: #14251a; padding: 12px 14px; margin-bottom: 14px; }
.bulk-result-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.bulk-result-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.bulk-result-list li { display: flex; flex-direction: column; gap: 3px; }

.col-select { width: 1%; white-space: nowrap; }
.admin-table td.col-thumb, .admin-table th.col-thumb { width: 56px; padding-right: 4px; }
.thumb-link { display: inline-flex; align-items: center; justify-content: center; width: 48px; height: 48px; border-radius: 6px; overflow: hidden; background: var(--panel-2); border: 1px solid var(--line); }
.thumb-link:hover { border-color: var(--accent); }
.thumb-mini { width: 100%; height: 100%; object-fit: cover; display: block; }
.thumb-link .type-icon { color: var(--muted); }
.thumb-link .type-glyph { width: 22px; height: 22px; }
.cell-size { color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.cell-tags { max-width: 320px; }

/* ---- M12 tag chips with remove ------------------------------------------ */
.tag-chip { display: inline-flex; align-items: center; gap: 4px; }
/* M89 L3 (V4): the capped tag chips wrap; the +K more control is a small inline link that the
   tag-more controller turns into an in-place toggle; the load error reads muted. */
.tag-chips { display: inline; }
.tag-chip__name { display: inline; }
.tag-more { display: inline-block; margin: 0 4px 4px 0; font-size: 12px; cursor: pointer; }
.tag-more-status { display: block; }
/* M26 L5 / M36: a hidden tag (only ever shown to the owner) reads muted with a dashed border.
   Declared BEFORE --restricted so a both-flags chip reads restricted (fill + text colour) while
   the hidden dashed border survives — restricted sets no border-style. This plain pair carries the
   full identity for the <span> chip branch (/s, admin chips), which has no hover rule. */
.tag-chip--hidden { background: var(--panel-2); color: var(--muted); border: 1px dashed var(--line); }
.tag-chip--restricted { background: #2a1616; color: #e58b8b; }
.chip-remove { display: inline; margin: 0; }
.chip-remove button {
    border: none; background: none; color: var(--muted); cursor: pointer;
    font: inherit; line-height: 1; padding: 0 2px; border-radius: 4px;
}
.chip-remove button:hover { color: #e58b8b; }

/* ---- M12 per-item edit popover + inline share --------------------------- */
.edit-pop summary { display: inline-flex; align-items: center; gap: 4px; }
.edit-body { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; padding: 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); }
.edit-body .field { margin-bottom: 6px; }

/* ---- M20b full-width edit row in the items table ------------------------- */
/* Actions and the edit form each get their own full-width row under the item (see .actions-row and
   .edit-row); there is no longer an actions column. */
/* The card layout keeps M12's grid edit body; the table layout uses a single column of forms. */
.edit-row > td { padding: 0 0 14px; }
.edit-row .edit-body { margin-top: 0; }
.share-inline { display: inline-flex; flex-direction: column; gap: 6px; }
.share-inline form { display: inline; margin: 0; }
.share-inline .btn { display: inline-flex; align-items: center; gap: 4px; }
.share-out { display: inline-flex; gap: 6px; align-items: center; }
.share-out .copy-field { width: 260px; max-width: 60vw; margin: 0; }
/* L1: the share-links list shows each link on its own full-width row with a copy button. */
.share-out--wide { display: flex; width: 100%; align-items: center; flex-wrap: wrap; }
.share-out--wide .copy-field { width: 100%; max-width: none; flex: 1 1 auto; }
.share-link-row > td { padding-top: 0; }

/* version/8 M93 A4: the Share links list — toolbar, scope cell, icon actions, collapsed URL row. */
.admin-toolbar { display: flex; flex-wrap: wrap; gap: 12px 24px; align-items: center; justify-content: space-between; margin: 0 0 14px; }
.jobs-toolbar { display: flex; flex-wrap: wrap; gap: 12px 24px; align-items: center; justify-content: space-between; margin: 0 0 18px; }
.jobs-state-facets { flex: 1 1 auto; }
.jobs-origin-filter { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 0; }
.jobs-origin-filter label { color: var(--muted); font-size: 13px; }
.jobs-origin-filter select { min-width: 170px; padding: 6px 9px; background: var(--panel-2); color: var(--text); border: 1px solid var(--line); border-radius: 7px; font: inherit; font-size: 13px; }
.narrowed-note { margin: 0 0 12px; }
.share-scope { display: flex; gap: 10px; align-items: flex-start; }
.share-scope--tags { flex-direction: column; gap: 4px; }
.share-scope-thumb { width: 48px; height: 48px; object-fit: cover; border-radius: 4px; flex: 0 0 auto; }
.share-scope-body { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.share-scope-name { font-weight: 600; overflow-wrap: anywhere; }
.share-scope-note, .share-scope-count { margin: 0; }
.share-scope-tags { display: flex; flex-wrap: wrap; gap: 4px; }
.share-actions { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.state-badge--active { border-color: #2f6b3f; color: #7fd39b; }
.state-badge--expired { opacity: 0.75; }
.state-badge--revoked { border-color: var(--warn); color: var(--warn); }
.btn-linkish { background: none; border: none; color: var(--accent); cursor: pointer; padding: 0; font: inherit; text-decoration: underline; }
.btn-linkish:hover { color: var(--text); }

/* ---- M12 preview metadata panel ----------------------------------------- */
.preview-head { display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap; margin-bottom: 18px; }
.preview-head-main { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
/* W1: wrap a long unbroken display name so it never overflows the preview head or the minigame panel
   (both render `.preview-name`), matching the sibling name classes. The minigame keeps this wrap (it
   has no download button to bump). */
.preview-name { margin: 0; font-size: 24px; overflow-wrap: anywhere; word-break: break-word; }
/* On the chrome header (/f, /s, /random — the pages that carry the Download button) a wrapped long name
   pushed the button down and opened a gap, so there the name is one line with an ellipsis and the full
   text on hover (the `title` attr). The whole header stays ONE row (nowrap): the name shrinks and
   ellipsizes so the badge and the right-aligned Download never drop to their own line. */
.preview-head--chrome { flex-wrap: nowrap; align-items: center; }
.preview-head--chrome .preview-head-main { flex: 1 1 auto; min-width: 0; flex-wrap: nowrap; }
.preview-head--chrome .preview-name { min-width: 0; flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; overflow-wrap: normal; word-break: normal; }
.preview-head--chrome .preview-badge { flex: 0 0 auto; }
.preview-head--chrome .download-btn { flex: 0 0 auto; }
.preview-head .download-btn { margin-left: auto; display: inline-flex; align-items: center; gap: 6px; }
/* Tag row directly under the header (below the name, above the file). On /f the chips are links. */
.preview-tags { display: flex; flex-wrap: wrap; gap: 6px; margin: -6px 0 18px; }
.preview-tags .tag-chip { margin: 0; }
/* M136 property chips: the fourth chip treatment. They use the ordinary panel and line colours, with a
   muted leading label; unlike tags they have no remove affordance and no minigame data attribute. */
.prop-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: -6px 0 18px; padding: 0; list-style: none; }
.prop-chips > li { display: flex; min-width: 0; max-width: 100%; }
.prop-chip {
    display: inline-flex; align-items: center; gap: 0; max-width: 100%;
    padding: 4px 8px; border: 1px solid var(--line); border-radius: 999px;
    background: var(--panel); color: var(--text); font-size: 12px; line-height: 1.25;
    text-decoration: none; overflow-wrap: anywhere;
}
a.prop-chip:hover { color: var(--text); border-color: var(--accent); text-decoration: none; }
.prop-chip__label { color: var(--muted); white-space: nowrap; }
.prop-chip__text { min-width: 0; overflow-wrap: anywhere; }
.prop-chip__mark { display: inline-flex; align-items: center; margin-left: 5px; color: var(--muted); }
/* C4 (S6, S-D18): in the minigame a tag chip above the file becomes a toggle for that tag's keep[] box. An
   unticked (about-to-be-removed) tag shows this OFF state so the owner can see what will survive the save.
   Inert everywhere else — only the minigame controller adds the class. */
.mg-stage .tag-chip { cursor: pointer; }
.mg-stage .tag-chip--off { opacity: .45; text-decoration: line-through; }
a.tag-chip { color: var(--text); text-decoration: none; border: 1px solid transparent; }
/* M36: raise the identity modifiers over a.tag-chip (:463, (0,1,1)) so the owner's restricted/hidden
   text colour and the hidden dashed border render on the LINKED /f chips. Placed BEFORE the :hover
   rule below: at (0,2,1) these TIE a.tag-chip:hover, so source order gives the state channels (color,
   border-color) to hover and leaves the identity channels (background, border-style) to these — a
   coloured chip keeps its hover affordance and stays readable as restricted or hidden while hovered.
   --hidden first so a both-flags chip reads restricted with the hidden dashed border still visible.
   Do NOT add an a.tag-chip.tag-chip--…:hover form — it would take the state channel back. */
a.tag-chip.tag-chip--hidden { color: var(--muted); border: 1px dashed var(--line); }
a.tag-chip.tag-chip--restricted { color: #e58b8b; }
a.tag-chip:hover { border-color: var(--accent); color: var(--text); }
/* M46 owner inline tag editor at the top of the preview. The view row holds the badges plus the
   "Edit tags" button on one line; the editor (hidden until the toggle) holds the shared chip input
   with Save/Cancel below it. */
.preview-tags-view { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 14px; }
.preview-tags-view .preview-tags { margin: 0; }
.preview-tags-empty { margin: 0; }
.preview-tags-edit-btn { padding: 2px 10px; font-size: 12px; }
.preview-tags-editor { margin-bottom: 18px; }
.preview-tags-editor .edit-form { margin-top: 0; }
.preview-tags-editor-actions { display: flex; gap: 8px; margin-top: 10px; }
.preview-badge {
    display: inline-flex; align-items: center; gap: 5px;
    padding: 3px 9px; border-radius: 999px; font-size: 12px; font-weight: 600;
    text-transform: uppercase; letter-spacing: .02em;
    color: var(--text); background: var(--panel-2); border: 1px solid var(--line);
}
/* V37 (M94 P2): the preview title-row badge follows the V33 rule — no badge for a still image/static WebP
   (gated in the template), the family colour otherwise. The nine families reuse the card-badge palette
   with white text on the solid fill. */
.preview-badge--video      { background: rgba(52, 88, 120, .9); color: #fff; border-color: transparent; }
.preview-badge--animated   { background: rgba(120, 74, 40, .9); color: #fff; border-color: transparent; }
.preview-badge--audio      { background: rgba(84, 66, 120, .9); color: #fff; border-color: transparent; }
.preview-badge--pdf        { background: rgba(120, 40, 52, .9); color: #fff; border-color: transparent; }
.preview-badge--text       { background: rgba(40, 100, 100, .9); color: #fff; border-color: transparent; }
.preview-badge--archive    { background: rgba(120, 96, 40, .9); color: #fff; border-color: transparent; }
.preview-badge--executable { background: rgba(96, 30, 30, .92); color: #fff; border-color: transparent; }
.preview-badge--design     { background: rgba(112, 44, 104, .9); color: #fff; border-color: transparent; }
.preview-badge--other      { background: rgba(70, 74, 82, .9); color: #fff; border-color: transparent; }
.meta-panel {
    margin: 24px 0 0; padding: 4px 0 0; border-top: 1px solid var(--line);
    display: grid; gap: 0;
}
.meta-row { display: grid; grid-template-columns: 180px 1fr; gap: 12px; padding: 10px 4px; border-bottom: 1px solid var(--line); }
.meta-row dt { display: inline-flex; align-items: center; gap: 8px; color: var(--muted); font-size: 13px; margin: 0; }
.meta-row dd { margin: 0; font-size: 14px; word-break: break-word; }
.meta-row dd .tag-chip { margin: 0 4px 4px 0; }
.meta-row dd .copy-field { display: block; margin: 4px 0 0; }
@media (max-width: 560px) {
    .meta-row { grid-template-columns: 1fr; gap: 2px; }
}

/* ---- M14 preview media: center + aspect-fit (ALL viewers, not gated) ----
   The body content is centered and every media element is bounded by the column width AND a
   viewport-relative height, keeping aspect ratio and never upscaling past natural size, so a large
   image/video fits on screen instead of overflowing the column. Applies on /f and /s alike. */
.preview-body { display: flex; justify-content: center; }
.preview-body > * { max-width: 100%; margin: 0 auto; }
/* Text/code/markdown/archive blocks FILL the centered column (only true media shrink-to-fit and
   center); a long code line scrolls inside its own block so the page body never scrolls sideways. */
.preview-render, .preview-archive, .preview-none { width: 100%; }
.preview-render--code, .preview-render--text { overflow-x: auto; }
.preview-image img,
.preview-video video,
.preview-video--gif img {
    display: block; margin: 0 auto;
    max-width: 100%; width: auto; height: auto;
    max-height: min(80vh, 900px);
    object-fit: contain;
}
.preview-note { text-align: center; }
/* Audio has no aspect ratio: just center it and cap the control width. */
.preview-audio { width: 100%; text-align: center; }
.preview-audio audio { width: 100%; max-width: 640px; }
/* V41 (M95 F4): the header line above the player, and the codec/type download note. */
.preview-audio-head { color: var(--muted); font-size: 13px; margin: 0 0 10px; word-break: break-word; }
.preview-audio-note { margin-top: 10px; }
/* M95 F5 (V42): the pdf.js viewer application runs in a same-origin iframe that scrolls internally, so
   the frame is a fixed fraction of the viewport height on every screen (it never grows with page count). */
.preview-pdf { width: 100%; max-width: 100%; }
.preview-pdf-frame { display: block; width: 100%; height: min(80svh, 900px); border: 1px solid var(--line); border-radius: 10px; background: var(--panel-2); }

/* ---- M95 F1 (V37, goal D4): phone order for the file page (/f, /s, /random) -----------------------
   Scoped to `main.preview--page` (the shell adds it under `chrome` or a `walkNav`), so the minigame
   play stage (chrome false, no walkNav — no `preview--page`) is untouched and keeps its own layout.
   Media first: title -> file body -> tags -> walk pill -> metadata. Every direct child gets an explicit
   `order`; an unlisted child (`> *`) falls to 99 and goes last. `.preview-derivations` also carries
   `.owner-controls`, so its rule sits AFTER the `.owner-controls` rule to win by source order. */
@media (max-width: 759px) {
    main.preview--page { display: flex; flex-direction: column; }
    main.preview--page > * { order: 99; }
    main.preview--page > .crumb { order: 1; }
    main.preview--page > .preview-head { order: 2; }
    main.preview--page > .preview-body { order: 3; }
    main.preview--page > .preview-tags-edit,
    main.preview--page > .preview-tags,
    main.preview--page > .prop-chips,
    main.preview--page > .preview-tags-empty { order: 4; }
    main.preview--page > .preview-gallery-nav { order: 5; }
    main.preview--page > .preview-album-nav { order: 6; }
    main.preview--page > .meta-panel { order: 7; }
    main.preview--page > .attribution { order: 8; }
    main.preview--page > .preview-ban-badge { order: 9; }
    main.preview--page > .owner-controls { order: 10; }
    main.preview--page > .preview-derivations { order: 11; }
    main.preview--page > .preview-siblings { order: 12; }
    main.preview--page > .nd-canonical-section { order: 13; }
}

/* ---- M95 F3 (V40): full-viewport video on a phone (/f, /s, /random only) --------------------------
   The player bleeds edge to edge, overriding the `.preview-body > *` column cap (line ~720) and the
   `.preview-video video { max-width: 100% }` cap (line ~726) — these rules have a higher specificity and
   sit after them, so they win. Scoped to main.preview--page, so the minigame play stage keeps its box.
   Desktop keeps `max-height: min(80vh, 900px)`. */
@media (max-width: 759px) {
    main.preview--page .preview-body > .preview-video { margin: 0 calc(50% - 50vw); width: 100vw; max-width: none; }
    main.preview--page .preview-video video { width: 100vw; max-width: none; max-height: 100svh; object-fit: contain; }
}

/* ---- M14/M59 owner controls on the preview page (ROLE_ADMIN, /f only) ------- */
.owner-controls { margin-top: 28px; padding-top: 4px; border-top: 1px solid var(--line); }
.owner-controls-label { display: inline-flex; align-items: center; gap: 8px; font-size: 15px; color: var(--muted); margin: 14px 0 10px; }
.owner-controls-locate, .random-locate, .owner-controls-edit { margin: 0 0 12px; font-size: 13px; }
.owner-controls-locate a, .random-locate a, .owner-controls-edit a { display: inline-flex; align-items: center; gap: 6px; }
/* M59 accordion: the triggers lay out as a wrapping button ROW; the open panel below spans the FULL
   owner-controls width. The `accordion` controller opens at most one panel at a time. */
.owner-triggers { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-start; }
.owner-trigger { cursor: pointer; display: inline-flex; align-items: center; gap: 6px; font-weight: 600; color: var(--accent); border: 1px solid var(--line); border-radius: 10px; background: var(--panel); padding: 8px 12px; font: inherit; }
.owner-trigger[aria-expanded="true"] { border-color: var(--accent); }
.owner-trigger--danger { color: #e58b8b; }
.owner-panels { margin-top: 12px; }
.owner-panel { width: 100%; max-width: none; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); padding: 12px 14px; }
.owner-card-body { margin-top: 10px; }
.owner-panel .edit-form { margin-top: 10px; }
.owner-panel .field textarea, .owner-panel .field select {
    padding: 8px 10px; background: var(--panel-2); color: var(--text);
    border: 1px solid var(--line); border-radius: 7px; font: inherit;
}
/* Kept for templates/admin/duplicates.html.twig — a native <details class="owner-card--danger">
   "Remove" card with an .owner-card-hint still consumes both selectors. Do not remove or repoint. */
.owner-card--danger summary { color: #e58b8b; }
.owner-card-hint { color: var(--muted); font-size: 13px; margin: 8px 0; }
.share-existing { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 8px; }
.share-existing .share-out { margin-bottom: 8px; }
.owner-panel .share-out { flex-wrap: wrap; }
.owner-panel .share-out .copy-field { width: 100%; margin: 0; }

/* ---- M39 shared-blob siblings panel (owner-only, below the owner controls) --- */
.preview-siblings { margin-top: 28px; padding-top: 12px; border-top: 1px solid var(--line); }
.preview-siblings-label { display: inline-flex; align-items: center; gap: 8px; font-size: 15px; color: var(--muted); margin: 0 0 6px; }
.sibling-list { list-style: none; margin: 10px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.sibling-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; padding: 6px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); }
.sibling-name { color: var(--accent); text-decoration: none; font-weight: 600; }
.sibling-name:hover { text-decoration: underline; }
.sibling-tags { display: flex; flex-wrap: wrap; gap: 4px; }
.sibling-untagged { font-style: italic; }

/* ---- M60 per-item derivations list (owner-only, below the owner controls) --- */
.preview-derivations .derivation-row {
    display: flex; align-items: flex-start; gap: 10px;
    padding: 8px 10px; margin-top: 8px;
    border: 1px solid var(--line); border-radius: 8px; background: var(--panel);
}
.preview-derivations .derivation-row .blob-thumb { flex: none; }
.preview-derivations .derivation-name { color: var(--accent); text-decoration: none; font-weight: 600; word-break: break-word; }
.preview-derivations .derivation-name:hover { text-decoration: underline; }
.preview-derivations .nd-group-meta { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 3px; }

/* ---- F3 (M74) in-place resolve controls on a ready asSource derivation ----- */
.derivation-resolve { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px; margin-top: 6px; }
.derivation-resolve form { margin: 0; }
.derivation-merge-confirm > summary { cursor: pointer; list-style: none; }
.derivation-merge-confirm > summary::-webkit-details-marker { display: none; }
.derivation-merge-confirm > .owner-card-body { margin-top: 8px; }
.derivation-merge-confirm > .owner-card-body form { margin-top: 8px; }

/* ---- M70 admin derivations page: bulk optimize/convert + row actions ------ */
.admin-subhead { font-size: 16px; font-weight: 600; color: var(--text); margin: 0 0 8px; }
.admin-subhead.small { font-size: 13px; font-weight: 600; color: var(--muted); margin: 16px 0 6px; }

.derivation-candidates {
    padding: 14px; margin: 0 0 22px;
    background: var(--panel-2); border: 1px solid var(--line); border-radius: 10px;
}
.derivation-candidates > p { margin-top: 0; }

/* The primary combined action: label(+select) sit inline with the submit button, wrapping cleanly. */
.derivation-bulk-form {
    display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px; margin: 0 0 8px;
}
.derivation-bulk-form label {
    display: inline-flex; flex-direction: column; gap: 4px;
    font-size: 12px; color: var(--muted);
}
.derivation-bulk-form select { width: auto; }

/* M74 F4: each per-kind candidate section STACKS — heading → controls → list → action buttons — so the
   Mode control and the action buttons never float beside the checkbox list. */
.derivation-candidate-section { margin-top: 16px; }
.derivation-candidate-section:first-of-type { margin-top: 8px; }

.derivation-candidate-form { margin: 0; }
.derivation-candidate-controls { margin: 0 0 8px; }
.derivation-candidate-controls label.small {
    display: inline-flex; flex-direction: column; gap: 4px;
    font-size: 12px; color: var(--muted);
}
.derivation-candidate-controls select { width: auto; }
.derivation-candidate-all-form { margin: 0; }
.derivation-candidate-actions { display: flex; flex-wrap: wrap; gap: 10px; margin: 10px 0 0; }
.derivation-candidate-actions .btn { margin: 0; }

.derivation-candidate-list {
    list-style: none; margin: 8px 0; padding: 0;
    display: flex; flex-direction: column; gap: 4px; max-height: 320px; overflow-y: auto;
}
.derivation-candidate-list li { margin: 0; }
.derivation-candidate-list label {
    display: flex; align-items: center; gap: 8px;
    padding: 4px 6px; border-radius: 6px;
}
.derivation-candidate-list label:hover { background: var(--panel); }
.derivation-candidate-list input[type="checkbox"] { width: auto; flex: none; }
.derivation-candidate-list .blob-thumb { width: 36px; height: 36px; flex: none; }
.derivation-candidate-list a { color: var(--accent); text-decoration: none; word-break: break-word; }
.derivation-candidate-list a:hover { text-decoration: underline; }

/* index rows: per-status accent + the ready-pair action cluster */
.derivation-row { border-left: 3px solid var(--line); }
.derivation-row--ready { border-left-color: #3f7a4f; }
.derivation-row--failed { border-left-color: #7a3f3f; }
.derivation-row--no_saving { border-left-color: var(--line); }
.derivation-actions { display: flex; flex-direction: column; gap: 6px; flex: none; }
.derivation-actions form { margin: 0; }
.derivation-rerun-form { margin: 6px 0 0; }

/* M90 C6 (V15): the derivations review row has its OWN layout — no shared nd-/dup- class. */
.drv-row { display: flex; align-items: flex-start; gap: 12px; padding: 12px 14px; margin-bottom: 12px;
    border: 1px solid var(--line); border-left: 3px solid var(--line); border-radius: 8px; background: var(--panel-2); }
.drv-thumb, .drv-thumb-placeholder { width: 64px; height: 64px; flex: none; object-fit: cover; border-radius: 8px; }
.drv-thumb-placeholder { display: inline-flex; align-items: center; justify-content: center; border: 1px dashed var(--line); }
.drv-body { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 4px; }
.drv-body a { overflow-wrap: anywhere; }
.drv-status { flex: none; }
.drv-actions { display: flex; flex-direction: column; gap: 6px; flex: none; }
.drv-actions form { margin: 0; }
@media (max-width: 759px) {
    .drv-row { flex-wrap: wrap; }
    .drv-actions { flex-direction: row; flex-wrap: wrap; width: 100%; }
}

/* ---- tag input autocomplete (typeahead dropdown across all tag fields) --- */
.ac-wrap { position: relative; display: block; width: 100%; }
.ac-wrap > input { width: 100%; }
.ac-menu {
    position: absolute; z-index: 30; top: 100%; left: 0; right: 0;
    margin: 2px 0 0; padding: 4px; list-style: none;
    max-height: 240px; overflow-y: auto;
    background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, .35);
}
.ac-opt {
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
    flex-wrap: wrap;
    padding: 6px 8px; border-radius: 6px; cursor: pointer; font-size: 14px; color: var(--text);
}
.ac-opt:hover, .ac-opt.is-active { background: var(--panel-2); }
.ac-opt--restricted .ac-opt-name { color: #e58b8b; }
.ac-opt-name { margin-right: auto; }
.ac-opt-flag {
    font-size: 11px; color: #e58b8b; background: #2a1616;
    border-radius: 999px; padding: 1px 8px; white-space: nowrap;
}
.ac-opt-count {
    font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
/* M62: the hidden-tag cue on a search-box suggestion (muted, italic — the label a hidden tag wears
   elsewhere). Distinct from the restricted flag; a both-flags row shows both. */
.ac-opt--hidden .ac-opt-name { color: var(--muted); font-style: italic; }
.ac-opt-flag--hidden { color: var(--muted); background: var(--panel-2); }
/* M99 T1: the typo-tolerant "Approximate match" cue on a fuzzy suggestion. A plain muted pill; the row
   wraps it to a second line at a narrow (400px) width rather than clipping. */
.ac-opt-approx, .tci-opt-approx, .mg-suggest-approx {
    font-size: 11px; color: var(--muted); background: var(--panel-2);
    border-radius: 999px; padding: 1px 8px; white-space: nowrap;
}

/* tag-rules R5: the "Rules" line under a chip input (what the groups expect of the chip set) and the
   `expects` hint on a suggestion row. Muted and advisory — nothing here blocks a save (D41). */
.tci-opt-expects { display: block; width: 100%; font-size: 11px; color: var(--muted); margin-top: 2px; }
.tci-rules { margin-top: 6px; display: flex; flex-direction: column; gap: 4px; font-size: 12px; }
.tci-rules-head { font-weight: 600; color: var(--muted); }
.tci-rules-group { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 6px; }
.tci-rules-group--may { opacity: 0.7; }
.tci-rules-name { color: var(--text); }
.tci-rules-label, .tci-rules-next .tci-rules-label { color: var(--muted); }
.tci-rules-next { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 6px; }
.tci-rules-chip {
    font-size: 11px; cursor: pointer; border: 1px solid var(--border);
    background: var(--panel-2); color: var(--text); border-radius: 999px; padding: 1px 8px;
}
.tci-rules-chip:hover { border-color: var(--accent); }
.tci-rules-chip--remove { color: var(--danger, #c0392b); }

/* tag-rules R6: the minigame rules panel + the picker rules block. */
.mg-rules-panel { margin: 0 0 10px; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--panel-2); }
.mg-rules-title { margin: 0 0 6px; font-size: 13px; }
.mg-rules-ok { margin: 0; }
.mg-rules-group { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 6px; margin-bottom: 4px; font-size: 12px; }
.mg-rules-group--may { opacity: 0.7; }
.mg-rules-name { font-weight: 600; }
.mg-rules-label { color: var(--muted); }
.mg-rules-chip {
    font-size: 11px; cursor: pointer; border: 1px solid var(--border);
    background: var(--panel); color: var(--text); border-radius: 999px; padding: 1px 8px;
}
.mg-rules-chip:hover { border-color: var(--accent); }
.mg-rules-chip--remove { color: var(--danger, #c0392b); }
.mg-rules-mode .mg-rules-group { margin-top: 4px; }

/* ---- M26 L6 keyboard-editable tag chip input --------------------------------------------
   A NEW class so the input-chip states (highlighted/editing/removable) never leak onto the display
   `.tag-chip` used on /s and the public preview. Colour variants mirror the display chip. */
/* Admin tags letter filter — spread edge to edge (justified) across the full width. */
.tag-letterbar {
    display: flex; flex-wrap: wrap; align-items: baseline;
    justify-content: space-between; gap: 4px 2px; margin: 0 0 14px;
}
.tag-letterbar a {
    padding: 2px 7px; border-radius: 6px; color: var(--muted);
    font-size: 13px; text-decoration: none; white-space: nowrap;
}
.tag-letterbar a:hover { color: var(--text); background: var(--panel-2); }
.tag-letterbar a.is-active { color: #fff; background: var(--accent); }

.tag-chip-input { position: relative; display: block; width: 100%; }
.tci-field {
    display: flex; flex-wrap: wrap; align-items: center; gap: 4px;
    padding: 4px 6px; min-height: 34px;
    background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px;
    cursor: text;
}
.tci-entry {
    flex: 1 1 60px; min-width: 60px; border: none; background: transparent;
    color: var(--text); padding: 2px; margin: 0; outline: none; font-size: 14px;
}
.tci-chip {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 1px 4px 1px 8px; border-radius: 999px;
    background: var(--panel); font-size: 12px; color: var(--text);
}
.tci-chip--restricted { background: #2a1616; color: #e58b8b; }
.tci-chip--hidden { background: var(--panel); color: var(--muted); }
.tci-chip.is-highlighted { outline: 2px solid var(--accent); }
.tci-chip-x {
    border: none; background: transparent; color: inherit; cursor: pointer;
    font-size: 15px; line-height: 1; padding: 0 2px;
}
.tci-chip-x:hover { color: var(--text); }
.tci-menu {
    position: absolute; z-index: 30; top: 100%; left: 0; right: 0;
    margin: 2px 0 0; padding: 4px; list-style: none;
    max-height: 240px; overflow-y: auto;
    background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, .35);
}
.tci-opt {
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
    flex-wrap: wrap;
    padding: 6px 8px; border-radius: 6px; cursor: pointer; font-size: 14px; color: var(--text);
}
.tci-opt:hover, .tci-opt.is-active { background: var(--panel-2); }
.tci-opt--restricted .tci-opt-name { color: #e58b8b; }
.tci-opt--hidden .tci-opt-name { color: var(--muted); }
.tci-opt-name { margin-right: auto; }
.tci-opt-flag {
    font-size: 11px; color: #e58b8b; background: #2a1616;
    border-radius: 999px; padding: 1px 8px; white-space: nowrap;
}
.tci-opt-count {
    font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* album-picker (M31 AJAX album typeahead) — mirrors the tag-input dropdown look with fsh tokens. */
.album-picker { position: relative; display: block; width: 100%; }
.album-picker-input {
    width: 100%; padding: 6px 30px 6px 9px; font: inherit; font-size: 13px;
    background: var(--panel-2); color: var(--text); border: 1px solid var(--line); border-radius: 7px;
}
.album-picker-clear {
    position: absolute; top: 0; right: 0; width: 2rem; height: 100%;
    display: inline-flex; align-items: center; justify-content: center;
    background: none; border: 0; cursor: pointer; font-size: 16px; line-height: 1;
    color: var(--muted);
}
.album-picker-clear:hover { color: var(--text); }
.album-picker-clear[hidden] { display: none; }
.album-picker-menu {
    position: absolute; z-index: 30; top: 100%; left: 0; right: 0;
    margin: 2px 0 0; padding: 4px; list-style: none;
    max-height: 240px; overflow-y: auto;
    background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, .35);
}
.album-picker-menu[hidden] { display: none; }
.album-picker-opt {
    padding: 6px 8px; border-radius: 6px; cursor: pointer; font-size: 14px; color: var(--text);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.album-picker-opt:hover, .album-picker-opt.is-active { background: var(--panel-2); }

/* ---- text-family previews: real containers + syntax coloring (preview-refinement) ----
   Code, plain text, and markdown are transformed to sanitized HTML on the app origin (M10) and were
   dropped into bare <div>s with no styling. These rules give each a padded, monospace, panel-backed,
   rounded, self-scrolling block that fits the M14 flex-centered .preview-body without the page body
   ever scrolling sideways (each block owns its horizontal scroll; long files cap at a viewport height
   and scroll vertically inside). No CSP change — style-src already allows an app stylesheet. */

/* Shared code/text block chrome. */
.preview-code,
.preview-text {
    margin: 0;
    padding: 14px 16px;
    background: var(--panel-2);
    color: var(--text);
    border: 1px solid var(--line);
    border-radius: 10px;
    font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
    font-size: 13px;
    line-height: 1.55;
    tab-size: 4;
    white-space: pre;
    overflow: auto;
    max-height: min(80vh, 900px);
}

/* The <pre class="preview-code"> owns the scroll; its wrapper must not add a second one. */
.preview-render--code { overflow: visible; }
.preview-render--code, .preview-render--markdown-wrap { width: 100%; }

/* M95 F6 (V44/V45): the unselectable line-number gutter. Every source line is a <span class="line">
   (CodeRenderer::renderLines and the markdown fence post-pass); a CSS counter numbers each line in a
   `user-select: none` gutter, so a copy of the block never includes the numbers. The "View raw" pane
   (<pre class="preview-code preview-code--raw">) has NO .line spans, so it gets no gutter. The kept `\n`
   between line spans keeps the block scrollable text under `white-space: pre`. */
.preview-code { counter-reset: line; }
.preview-code .line::before {
    counter-increment: line;
    content: counter(line);
    display: inline-block;
    width: 3ch;
    margin-right: 1.5ch;
    padding-right: 1ch;
    border-right: 1px solid var(--line);
    color: var(--muted);
    text-align: right;
    user-select: none;
    -webkit-user-select: none;
}

/* "View raw" toggle (JS-free <details>) for markdown and code — no inline script, CSP unchanged. */
.preview-raw { margin-top: 12px; }
.preview-raw > summary {
    cursor: pointer; display: inline-flex; align-items: center; gap: 6px;
    color: var(--accent); font-size: 13px; user-select: none;
}
.preview-raw > summary:hover { text-decoration: underline; }
.preview-raw .preview-code--raw { margin-top: 10px; }
.preview-code--raw { color: var(--muted); }

/* tempest/highlight token palette — copied from vendor/tempest/highlight/src/Themes/Css/github-dark.css
   (chosen 2026-08-23) and tuned to the app's dark panel. Copied, not linked, so the palette is owned
   by the app and survives a vendor bump. Scoped under .preview-code so the highlighter spans never
   restyle admin inline <code>. The vendor `pre, code { background }` rule is deliberately dropped —
   .preview-code sets its own panel background above. */
.preview-code .hl-keyword  { color: #f97583; }
.preview-code .hl-property { color: #79b8ff; }
.preview-code .hl-attribute { font-style: italic; }
.preview-code .hl-type     { color: #b392f0; }
.preview-code .hl-generic  { color: #b392f0; }
.preview-code .hl-value    { color: #9ecbff; }
.preview-code .hl-literal  { color: #9ecbff; }
.preview-code .hl-number   { color: #9ecbff; }
.preview-code .hl-variable { color: #ffab70; }
.preview-code .hl-comment  { color: #6a737d; font-style: italic; }
.preview-code .hl-blur     { filter: blur(2px); }
.preview-code .hl-strong   { font-weight: bold; }
.preview-code .hl-em       { font-style: italic; }

/* Markdown typography — scoped to .preview-render--markdown so it can't leak into admin chrome. */
.preview-render--markdown {
    width: 100%;
    max-width: 80ch;
    margin: 0 auto;
    text-align: left;
    color: var(--text);
    overflow-wrap: break-word;
}
.preview-render--markdown > :first-child { margin-top: 0; }
.preview-render--markdown > :last-child { margin-bottom: 0; }
.preview-render--markdown h1,
.preview-render--markdown h2,
.preview-render--markdown h3,
.preview-render--markdown h4 { line-height: 1.25; margin: 1.4em 0 .6em; }
.preview-render--markdown h1 { font-size: 1.7em; border-bottom: 1px solid var(--line); padding-bottom: .3em; }
.preview-render--markdown h2 { font-size: 1.4em; border-bottom: 1px solid var(--line); padding-bottom: .3em; }
.preview-render--markdown h3 { font-size: 1.2em; }
.preview-render--markdown p,
.preview-render--markdown ul,
.preview-render--markdown ol { margin: 0 0 1em; }
.preview-render--markdown ul,
.preview-render--markdown ol { padding-left: 1.6em; }
.preview-render--markdown li { margin: .25em 0; }
.preview-render--markdown a { color: var(--accent); }
.preview-render--markdown code {
    background: var(--panel-2); padding: 1px 5px; border-radius: 4px; font-size: .9em;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.preview-render--markdown pre {
    background: var(--panel-2); border: 1px solid var(--line); border-radius: 10px;
    padding: 14px 16px; overflow: auto; max-height: min(70vh, 720px);
}
.preview-render--markdown pre code { background: none; padding: 0; font-size: 13px; }
.preview-render--markdown blockquote {
    margin: 0 0 1em; padding: 2px 14px; border-left: 3px solid var(--line); color: var(--muted);
}
.preview-render--markdown hr { border: 0; border-top: 1px solid var(--line); margin: 1.5em 0; }
.preview-render--markdown img { max-width: 100%; height: auto; }
.preview-render--markdown table { border-collapse: collapse; width: 100%; margin: 0 0 1em; display: block; overflow-x: auto; }
.preview-render--markdown th,
.preview-render--markdown td { border: 1px solid var(--line); padding: 6px 10px; text-align: left; }
.preview-render--markdown th { background: var(--panel-2); }

/* ---- M22 duplicates page ------------------------------------------------- */
.dup-group {
    border: 1px solid var(--line); border-radius: 10px; background: var(--panel);
    padding: 14px 16px; margin: 0 0 18px;
}
.dup-group-head { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
.dup-group-head .blob-thumb { width: 64px; height: 64px; }
.dup-thumb-placeholder {
    width: 64px; height: 64px; border-radius: 6px; background: var(--panel-2);
    display: flex; align-items: center; justify-content: center;
}
.dup-group-meta { min-width: 0; }
.dup-sha { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 60ch; }
.dup-items { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.dup-item {
    display: flex; align-items: flex-start; gap: 12px;
    padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2);
}
.dup-item.is-restricted { box-shadow: inset 3px 0 0 #c0524f; }
.dup-keep {
    display: inline-flex; flex-direction: column; align-items: center; gap: 2px;
    font-size: 11px; color: var(--muted); cursor: pointer; padding-top: 2px;
}
.dup-item-body { flex: 1 1 auto; min-width: 0; }
.dup-name { font-weight: 600; }
.dup-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 4px 0; }
.dup-sub { word-break: break-word; }
.dup-remove { margin-left: auto; }
.dup-remove > summary { list-style: none; cursor: pointer; }
.dup-remove > summary::-webkit-details-marker { display: none; }
.dup-remove-body { margin-top: 8px; }
.dup-source-pick {
    margin: 12px 0 0; padding: 10px 12px; border: 1px solid var(--line); border-radius: 8px;
    background: var(--panel-2); display: flex; flex-direction: column; gap: 8px;
}
.dup-source-pick legend { padding: 0 4px; font-size: 12px; color: var(--muted); }
.dup-source-option { display: flex; align-items: flex-start; gap: 8px; cursor: pointer; }
.dup-source-option > span { display: flex; flex-direction: column; min-width: 0; }
.dup-source-url { word-break: break-all; }
.dup-summary { margin: 10px 0 0; }
.dup-actions { margin-top: 12px; }
/* D1: the merge button is pushed to the right; the explanation is the `?` help chip to its left. */
.dup-bulk-simple { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 0 0 18px; justify-content: flex-end; }
.dup-bulk-simple form { margin: 0; }
.dup-bulk-help {
    display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; border: 1px solid var(--line); border-radius: 999px;
    font-size: 12px; line-height: 1; cursor: help; background: var(--panel);
}
.dup-bulk-help:hover, .dup-bulk-help:focus { color: var(--text); border-color: var(--accent); }

/* ---- M90 C2 (V9/V10): the exact-duplicate ledger (variant-B two-column) ---- */
.dup-ledger {
    display: grid; grid-template-columns: 200px 1fr; gap: 16px; align-items: start;
    border: 1px solid var(--line); border-radius: 10px; background: var(--panel);
    padding: 14px 16px; margin: 0 0 18px;
}
.dup-ledger-blob { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.dup-ledger-blob .blob-thumb { width: 64px; height: 64px; }
.dup-ledger-copies { min-width: 0; }
/* the merge form only carries hidden fields, so it must not occupy a grid cell */
.dup-merge-form { display: contents; }
.dup-cat { align-self: flex-start; }
.dup-item { flex-wrap: wrap; }
.dup-item > .btn-icon { margin-left: auto; flex: none; }
.dup-remove-form { flex: 0 0 100%; margin: 0; }
.dup-remove-form .inline-confirm { margin: 8px 0 0; }
.dup-fold-toggle { margin: 8px 0 0; }
@media (max-width: 759px) {
    /* the blob column becomes a header strip; controls stack; the merge button is full width */
    .dup-ledger { grid-template-columns: 1fr; }
    .dup-ledger-blob { flex-direction: row; flex-wrap: wrap; align-items: center; }
    .dup-actions .btn { width: 100%; }
}

/* Tag stats — a plain VERTICAL bar histogram, one column per tag-count bucket (0..25, then >25). */
.tag-hist {
    display: flex; align-items: flex-end; gap: 4px; height: 480px; margin: 12px 0 6px;
    padding: 0 2px; overflow-x: auto;
}
.tag-hist-col { flex: 1 1 0; min-width: 18px; height: 100%; display: flex; flex-direction: column; align-items: center; }
.tag-hist-val { height: 14px; font-size: 10px; color: var(--muted); font-variant-numeric: tabular-nums; line-height: 14px; }
.tag-hist-bar-wrap { flex: 1 1 auto; width: 100%; display: flex; align-items: flex-end; }
.tag-hist-bar { width: 100%; background: var(--accent); border-radius: 4px 4px 0 0; min-height: 2px; }
.tag-hist-bar.is-empty { min-height: 0; }
.tag-hist-x { margin-top: 4px; font-size: 10px; color: var(--muted); font-variant-numeric: tabular-nums; }
.tag-hist-col:hover { text-decoration: none; }
.tag-hist-col:hover .tag-hist-bar { filter: brightness(1.15); }
.tag-hist-delta { height: 12px; font-size: 9px; line-height: 12px; }

/* V20 T4: real-count grid lines behind the square-root bars. The overlay spans the bar-wrap band (below
   the value/delta rows, above the x-axis labels), so a line at √v/√max maps onto the same scale the bars
   use. The plot keeps its 480px height (T-D14). */
.tag-hist-plot { position: relative; }
.tag-hist-grid { position: absolute; left: 0; right: 0; top: 26px; bottom: 18px; pointer-events: none; }
.tag-hist-gridline { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--line); }
.tag-hist-gridlabel {
    position: absolute; left: 0; top: -0.9em; font-size: 10px; color: var(--muted);
    background: var(--panel); padding: 0 4px; font-variant-numeric: tabular-nums;
}

/* V20 T4: per-bucket / per-tile change marks. */
.delta { font-size: 0.72em; font-variant-numeric: tabular-nums; white-space: nowrap; }
.delta.is-up { color: #6fce8f; }
.delta.is-down { color: #e58b8b; }
.delta.is-flat { color: var(--muted); }

/* C7: the per-snapshot top-tags block. A three-part row (rank, name+bar, count) that stays on one line and
   truncates a long name rather than widening — so the block never scrolls the page sideways at phone width. */
/* M143 S2: the second ("Tags by number of items") distribution block + the "not recorded" tags tile. */
.tag-dist-block { margin-top: 22px; }
.tag-dist-heading { font-size: 15px; margin: 0 0 6px; }
.stat-num--missing { font-size: 15px; font-weight: 600; color: var(--muted); }

.top-tags-block { margin-top: 22px; }
.top-tags-heading { font-size: 15px; margin: 0 0 6px; }
.top-tags-list { list-style: none; margin: 8px 0 0; padding: 0; }
.top-tags-list li.top-tag {
    display: grid; column-gap: 10px; row-gap: 2px; align-items: center;
    grid-template-columns: 2.2em minmax(0, 1fr) auto;
    grid-template-areas: "rank name count" "rank bar count";
    padding: 4px 0; border-bottom: 1px solid var(--line);
}
.top-tag-rank { grid-area: rank; color: var(--muted); font-variant-numeric: tabular-nums; text-align: right; font-size: 12px; }
.top-tag-name { grid-area: name; min-width: 0; display: flex; align-items: center; gap: 6px; }
.top-tag-name > a, .top-tag-name > .past-name {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.top-tag-name .past-name { color: var(--muted); font-style: italic; }
.top-tag.is-restricted .top-tag-name > a { color: #e58b8b; }
.top-tag.is-hidden .top-tag-name > a { opacity: 0.8; }
.tag-flag { font-size: 0.85em; flex: none; }
.top-tag-bar-wrap { grid-area: bar; height: 4px; background: var(--line); border-radius: 2px; overflow: hidden; }
.top-tag-bar { display: block; height: 100%; background: var(--accent); border-radius: 2px; }
.top-tag-count { grid-area: count; font-variant-numeric: tabular-nums; font-size: 12px; white-space: nowrap; text-align: right; }
.rankmove { font-size: 0.72em; font-variant-numeric: tabular-nums; white-space: nowrap; }
.rankmove.is-up { color: #6fce8f; }
.rankmove.is-down { color: #e58b8b; }
.rankmove.is-flat { color: var(--muted); }
.rankmove.is-new { color: var(--accent); }
.rankmove.is-notcaptured { color: var(--muted); font-style: italic; }
.top-tags-all { margin-top: 8px; }
.top-tags-all > summary { cursor: pointer; color: var(--accent); font-size: 13px; }

/* tag-leaderboards B2: the two leaderboards side by side on wide screens, stacked at phone width. Each
   column has a 280px floor, so below ~600px the grid collapses to one column (no page-level side scroll at
   375px: 280px + the 16px gutters fit). */
.top-tags-boards { display: grid; gap: 0 28px; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); align-items: start; }

/* V20 T4: the comparison toolbar. */
.stats-toolbar { display: flex; flex-wrap: wrap; gap: 10px 20px; align-items: center; margin: 14px 0; }
.stats-compare-form { display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: center; margin: 0; }
.tag-hist-table { margin-top: 8px; }
.tag-hist-table > summary { cursor: pointer; color: var(--accent); font-size: 13px; }

/* Visually-hidden label kept for assistive tech (the row-select checkboxes, table caption). */
.sr-only {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;
    clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Tag Venn (M46) — the row-select scaffold on the list, then the region table + schematic circles. */
.tag-select-col { width: 28px; text-align: center; }
.tag-select-bar { display: flex; align-items: center; gap: 10px; margin: 0 0 10px; }
/* M55 Y2 — tags list toolbar: the create form and the find/access filter share one aligned band,
   create on the left and filter pushed to the right, wrapping onto two rows on narrow screens. The
   inner .inline-form margins are dropped so the band controls the spacing. */
.tag-toolbar { display: flex; flex-wrap: wrap; gap: 12px 24px; align-items: flex-end; justify-content: space-between; margin: 0 0 14px; }
.tag-toolbar .inline-form { margin: 0; }
/* The visualize / merge select bar reads as its own grouped panel above the table. */
.tag-select-bar { flex-wrap: wrap; padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); }
/* M55 Y3 — the per-row action cell is a tight horizontal cluster of icon controls (restricted toggle,
   hide toggle, delete, delete items, details link). Each .btn-icon carries a title/aria-label; the
   destructive controls take a red icon tint (currentColor) on top of the danger border. */
.tag-actions { white-space: nowrap; }
.tag-actions form { display: inline-flex; margin: 0; }
.tag-actions .btn-icon { display: inline-flex; align-items: center; vertical-align: middle; min-height: 34px; padding-top: 6px; padding-bottom: 6px; }
.tag-actions .btn-danger { color: #e58b8b; }
/* M91 T1 — the item-count range inputs on the filter row and the create panel collapsed behind ＋ New tag
   (its own full-width row within the toolbar flexbox when revealed). */
.tag-count-input { width: 5.5em; }
.tag-create-panel { flex-basis: 100%; }
.tag-create-panel .inline-form { margin: 0; flex-wrap: wrap; }
.venn-selected { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0 14px; }
.venn-chip {
    display: inline-flex; align-items: center; gap: 6px; padding: 2px 10px; border-radius: 999px;
    border: 1px solid var(--line); background: var(--panel-2); font-size: 12px;
}
.venn-chip .venn-swatch { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
.venn-chip.is-restricted { border-color: #7a3a3a; }
.venn-chip.is-hidden { border-color: var(--muted); }
.venn-table { max-width: 640px; }
.venn-table td.venn-count, .venn-table th.venn-count { text-align: right; font-variant-numeric: tabular-nums; }
.venn-table tr.is-empty td { color: var(--muted); }
.venn-region-label { display: inline-flex; flex-wrap: wrap; gap: 4px 6px; align-items: center; }
.venn-region-op { color: var(--muted); }
.venn-totals { max-width: 640px; margin-top: 18px; }
.venn-svg-wrap { margin: 8px 0 20px; max-width: 100%; overflow-x: auto; }
.venn-svg { max-width: 440px; }
.venn-svg .venn-circle { fill-opacity: .16; stroke-width: 2; }
.venn-svg text { fill: var(--text); font-size: 13px; font-variant-numeric: tabular-nums; }
.venn-svg text.venn-svg-name { fill: var(--muted); font-size: 11px; }
/* M39 B6 — group category label, subset-merge include control, and the album-group explanation. */
.dup-cat {
    margin-left: auto; align-self: flex-start; padding: 1px 9px; border-radius: 999px;
    border: 1px solid var(--line); font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
    color: var(--muted);
}
.dup-cat--album { border-color: #3a5a7a; color: #8fbfe0; }
.dup-cat--reconcile { border-color: #7a6a2f; color: #d9c489; }
.dup-include {
    display: inline-flex; flex-direction: column; align-items: center; gap: 2px;
    font-size: 11px; color: var(--muted); cursor: pointer; padding-top: 2px;
}
.dup-note { margin: 12px 0 0; font-size: 12px; }
.badge-album { border-color: #3a5a7a; color: #8fbfe0; display: inline-flex; align-items: center; gap: 4px; }

/* ---- Albums (version/3): reorderable member list, admin album list, preview prev/next ---------- */

/* The reorderable member list — shared by the upload tagging page (gallery mode) and the album
   detail page. Rows are draggable; the `sortable` controller reveals the handle + ↑/↓ (rendered
   hidden, so nothing looks live with JS off). */
.album-list { list-style: none; margin: 0 0 16px; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.album-row {
    display: flex; align-items: center; gap: 10px;
    padding: 6px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2);
}
.album-row.is-dragging { opacity: .5; border-color: var(--accent); }
.album-row.is-cover { border-color: #2f6b3f; }
.album-row-controls { display: inline-flex; align-items: center; gap: 4px; }
.album-row-controls[hidden] { display: none; }
.drag-handle { cursor: grab; color: var(--muted); user-select: none; }
.btn-icon { padding: 2px 8px; line-height: 1.2; }
/* M88 S6 (V4): a row/card icon action glyph is 16px inside a .btn-icon. */
.btn-icon .act-ico { width: 16px; height: 16px; }
.album-pos { min-width: 1.6em; text-align: right; color: var(--muted); font-variant-numeric: tabular-nums; }
.album-thumb { width: 40px; height: 40px; object-fit: cover; border-radius: 6px; border: 1px solid var(--line); }
.album-order { width: 4.5em; }
.album-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.album-remove { margin-left: auto; }
.album-delete-form { margin-top: 24px; }

/* The /admin/albums list — a card grid of covers. */
.album-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 14px; margin: 0 0 20px;
}
.album-card {
    display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: 10px;
    background: var(--panel); overflow: hidden;
}
.album-card:hover { border-color: var(--accent); text-decoration: none; }
.album-card-thumb {
    aspect-ratio: 4 / 3; background: var(--panel-2); display: flex; align-items: center; justify-content: center;
}
.album-card-thumb img { width: 100%; height: 100%; object-fit: cover; }
.album-card-noimg { color: var(--muted); }
.album-card-body { padding: 8px 10px; display: flex; flex-direction: column; gap: 4px; }
.album-card-title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: inherit; text-decoration: none; }
.album-card-title:hover { text-decoration: underline; }

/* M89 L5 (V6): the three-tile mosaic (one lead tile + two small), search box, tag chips and id copy. */
.album-card-mosaic {
    display: grid; grid-template-columns: 2fr 1fr; grid-template-rows: 1fr 1fr; gap: 2px;
    aspect-ratio: 4 / 3; background: var(--panel-2); text-decoration: none;
}
.album-card-mosaic .album-tile { background: var(--panel-2); overflow: hidden; }
.album-card-mosaic .album-tile img { width: 100%; height: 100%; object-fit: cover; }
.album-card-mosaic .album-tile--lead { grid-row: span 2; }
.album-card-mosaic .album-card-noimg { grid-column: 1 / -1; grid-row: 1 / -1; display: flex; align-items: center; justify-content: center; color: var(--muted); }
.album-card-tags { display: flex; flex-wrap: wrap; gap: 4px; font-size: 12px; }
.album-tag { padding: 1px 6px; border: 1px solid var(--line); border-radius: 999px; background: var(--panel-2); }
.album-search { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 16px; }
.album-search input[type="search"] { flex: 1 1 220px; min-width: 0; padding: 6px 10px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; color: var(--text); }
.copy-album-id { width: 100%; font-family: ui-monospace, monospace; font-size: 11px; padding: 2px 6px; background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px; color: var(--muted); }
.album-id-line { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; margin: 0 0 10px; }
.album-id-line .copy-album-id { flex: 0 1 320px; }
.album-move { }

/* The collections toolbar: the C-D5 sort, the C-D6 list/gallery switch and the C5 "Recount all" on ONE
   aligned row that wraps cleanly on a phone (D4), the groups spaced apart and "Recount all" pushed to the
   trailing edge. Without this the three groups were unstyled blocks that stacked one per line. */
.collections-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; margin: 0 0 16px; }
.collections-sort, .collections-view-switch { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.recount-all-form { margin-inline-start: auto; }
/* The chosen sort and view read as selected (there is no global .btn.is-active): accent border, full text. */
.collections-controls .btn.is-active { border-color: var(--accent); color: var(--text); background: var(--panel); font-weight: 600; }

/* The collections bulk action bar (C4 scoped tag op + C5 recount + merge): a bordered bar like .bulk-bar,
   one row that wraps, with the two tag fields BOUNDED (not full-width) so "Operation", Add, Remove and the
   buttons line up instead of each sprawling onto its own row. */
.collections-actionbar {
    display: flex; flex-wrap: wrap; align-items: center; gap: 10px 12px;
    padding: 10px 14px; margin: 0 0 14px;
    border: 1px solid var(--line); border-radius: 10px; background: var(--panel);
}
.collections-op, .collections-op-field { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); font-size: 13px; }
.collections-op select {
    padding: 6px 9px; background: var(--panel-2); color: var(--text);
    border: 1px solid var(--line); border-radius: 7px; font: inherit; font-size: 13px;
}
.collections-op-field .tag-chip-input-host { width: 200px; }
/* Phone (D4): let each tag field take its own full-width row inside the wrapped bar. */
@media (max-width: 759px) {
    .collections-op-field { flex: 1 1 100%; }
    .collections-op-field .tag-chip-input-host { width: 100%; flex: 1 1 auto; }
}

/* The album public-state badge (M83 U1, V7): an explanation, not a control. `public` reads as accent,
   every hidden case reads muted. It wraps with its reason on a phone (D4) with no horizontal scroll. */
.album-badge {
    display: inline-block; margin: 4px 0 2px; padding: 1px 8px; border-radius: 999px;
    font-size: .78rem; line-height: 1.5; border: 1px solid var(--line); background: var(--panel-2);
    color: var(--muted); max-width: 100%; overflow-wrap: anywhere;
}
.album-badge--listed { color: var(--accent); border-color: var(--accent); }
.album-badge--hidden_restricted { color: #e58b8b; border-color: #7a2f2f; }
.album-public-state {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px; margin: 0 0 16px;
}

/* The preview page's album prev/next (a /f affordance only). */
.preview-album-nav {
    display: flex; align-items: center; justify-content: center; gap: 16px;
    margin: 12px 0 0; padding: 8px 12px;
    border: 1px solid var(--line); border-radius: 999px; background: var(--panel-2);
}
.album-nav-link { white-space: nowrap; }
.album-nav-link.is-disabled { color: var(--muted); opacity: .5; cursor: default; }
.album-nav-counter { color: var(--muted); font-variant-numeric: tabular-nums; }

/* M26 L7 gallery walk nav — same pill look as the album nav, shown in addition to it when both apply. */
.preview-gallery-nav {
    display: flex; align-items: center; justify-content: center; gap: 16px;
    margin: 12px 0 0; padding: 8px 12px;
    border: 1px solid var(--line); border-radius: 999px; background: var(--panel-2);
}
.gallery-nav-link { white-space: nowrap; }
.gallery-nav-link.is-disabled { color: var(--muted); opacity: .5; cursor: default; }

/* M26 L7 click-to-enlarge lightbox overlay. */
.lightbox-overlay {
    position: fixed; inset: 0; z-index: 1000;
    display: flex; align-items: center; justify-content: center;
    background: rgba(0, 0, 0, .85); cursor: zoom-out;
}
.lightbox-img {
    max-width: 96vw; max-height: 96vh; object-fit: contain; cursor: default;
    box-shadow: 0 10px 40px rgba(0, 0, 0, .5);
}
.lightbox-close {
    position: fixed; top: 14px; right: 18px;
    width: 40px; height: 40px; border-radius: 999px;
    border: 1px solid rgba(255, 255, 255, .3); background: rgba(0, 0, 0, .5);
    color: #fff; font-size: 26px; line-height: 1; cursor: pointer;
}
.lightbox-close:hover { background: rgba(0, 0, 0, .75); }

/* ---- Share-link mint page (/admin/share-links/new) — version/8 M93 A6, two-step ------------
   Step 1 is two choice cards; step 2 is the admin gallery query (item) or the tag chip input +
   grant preview (tags). The tokens match the rest of the admin surface. */
.scope-choice-cards { display: flex; flex-wrap: wrap; gap: 16px; margin: 8px 0 16px; }
.scope-card {
    display: flex; flex-direction: column; gap: 6px; flex: 1 1 240px; min-width: 220px;
    padding: 18px 20px; border: 1px solid var(--line); border-radius: 12px; background: var(--panel);
    color: var(--text); text-decoration: none;
}
.scope-card:hover { border-color: var(--accent); background: var(--panel-2); }
.scope-card-title { font-size: 17px; font-weight: 600; }

/* Grouping fieldsets on the mint form — a light panel per section, with the legend as its label. */
.tag-scope, .ttl-choice {
    border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; margin: 0 0 16px;
    max-width: 560px;
}
.tag-scope legend, .ttl-choice legend { padding: 0 6px; color: var(--muted); font-size: 13px; }
.ttl-choice { display: flex; flex-wrap: wrap; gap: 12px 18px; }
.ttl-choice legend { flex-basis: 100%; }
.ttl-choice .radio { display: inline-flex; align-items: center; gap: 6px; }
.ttl-choice input[type="number"] { width: 80px; }
.share-pick { display: flex; flex-direction: column; gap: 6px; }
.share-pick.is-picked { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 12px; }
.share-pick-radio { display: inline-flex; align-items: center; gap: 6px; font-size: 14px; }
.share-pick-radio input { width: auto; }
.grant-count { margin: 10px 0; font-size: 15px; }

/* -------- M31 tagging minigame ------------------------------------------------------------------ */
/* Picker */
.mg-picker { display: grid; grid-template-columns: 2fr 1fr; gap: 18px; align-items: start; }
.mg-picker .btn { grid-column: 1 / -1; justify-self: start; }
@media (max-width: 759px) { .mg-picker { grid-template-columns: 1fr; } }
.mg-rule { border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; }
.mg-rule legend { padding: 0 6px; color: var(--muted); font-size: 13px; }
.mg-radio { display: flex; align-items: center; gap: 8px; font-size: 14px; }
.mg-radio input[type="radio"] { width: auto; }
.mg-subrule { margin: 2px 0 6px 24px; display: flex; flex-direction: column; gap: 8px; }
.mg-hint { color: var(--muted); font-size: 12px; margin: 2px 0 0; }
.mg-n { width: 80px; }

/* Play — 70/30 split */
.mg-play { display: grid; grid-template-columns: minmax(0, 7fr) minmax(280px, 3fr); gap: 20px; align-items: start; }
.mg-preview { min-width: 0; border: 1px solid var(--line); border-radius: 10px; padding: 4px 16px 16px; }
.mg-preview .preview { max-width: none; margin: 0; }
.mg-panel { position: sticky; top: 16px; }
.mg-form { display: flex; flex-direction: column; gap: 14px; }
.mg-count { margin: 0; font-size: 14px; color: var(--muted); }
.mg-count strong { color: var(--text); font-size: 16px; }
.mg-block { border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; }
.mg-block legend { padding: 0 6px; color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .06em; }
/* K4: the Album / Source sections collapse via native <details>. A <details> is not flex (its
   open/close animation and summary want plain block flow), so it overrides the flex .mg-block base and
   spaces its body with .mg-details-body instead; the summary is styled like a legend. */
details.mg-block { display: block; }
details.mg-block > summary { padding: 0 2px; color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .06em; cursor: pointer; list-style: none; }
details.mg-block > summary::-webkit-details-marker { display: none; }
details.mg-block > summary::before { content: '▸ '; }
details.mg-block[open] > summary::before { content: '▾ '; }
.mg-details-body { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }
/* K2: album neighbours — "this file belongs to a set" — above the edit fieldsets. */
.mg-neighbours { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 12px; color: var(--muted); border: 1px solid var(--line); border-radius: 10px; padding: 6px 10px; }
.mg-neighbour-link { color: var(--accent); text-decoration: none; white-space: nowrap; }
.mg-neighbour-link:hover { text-decoration: underline; }
.mg-neighbour-link.is-disabled { color: var(--muted); opacity: .5; pointer-events: none; }
.mg-neighbour-set { text-align: center; }
.mg-neighbour-set strong { color: var(--text); }

/* Q1: the upload-order neighbours as gallery file-box cards, in a full-width row below the play grid. */
.mg-neighbour-cards { margin-top: 20px; }
.mg-neighbour-cards-title { font-size: 13px; font-weight: 600; color: var(--muted); margin: 0 0 10px; }
.mg-neighbour-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 240px)); }
.mg-neighbour-card { position: relative; }
.mg-neighbour-card-label {
    display: block; font-size: 12px; color: var(--muted); padding: 6px 10px 0; white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis;
}
.mg-input { width: 100%; }
/* Theme every minigame text / number / select / textarea control (they were default browser-styled —
   the "unstyled" white boxes). Radios/checkboxes keep their native look (styled via .mg-radio/.mg-chip);
   the album-picker input styles itself (.album-picker-input) to leave room for its × button. */
.mg-picker select, .mg-n,
input.mg-input, textarea.mg-input, select.mg-input {
    padding: 6px 9px; background: var(--panel-2); color: var(--text);
    border: 1px solid var(--line); border-radius: 7px; font: inherit; font-size: 13px;
}
textarea.mg-input { resize: vertical; }
.mg-empty { margin: 0; }
.mg-taglist { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
.mg-chip { display: inline-flex; align-items: center; gap: 6px; padding: 2px 9px; border-radius: 999px; background: var(--panel-2); border: 1px solid var(--line); font-size: 12px; cursor: pointer; }
.mg-chip input { width: auto; }
/* M39 B3 — three CSS channels, one declaration order (see the plan's "CSS channel mechanics"): all of
   these are single-class selectors of EQUAL specificity, so source order decides. .mg-chip--add is
   declared FIRST of the four so the flag tokens' background/colour (declared after it) win over its
   muted colour; the --src-* source tokens are declared LAST so their border-colour wins; the add-chip's
   dashed border-style is set by no later rule, so it survives under a source colour.
     border-color = the suggestion SOURCE (--src-related / --src-album / --src-sibling)
     background + color = the tag FLAG (--restricted / --hidden)
     border-style = the ADD affordance (--add) */
.mg-chip--add { border-style: dashed; color: var(--muted); }
.mg-chip--add:hover { color: var(--text); border-color: var(--accent); }
/* --hidden takes a channel free on an add-chip (already muted + dashed): a distinct neutral fill plus a
   non-colour marker (italic). It MUST NOT touch border-color (source) or border-style (add). */
.mg-chip--hidden { background: var(--line); color: var(--muted); font-style: italic; }
.mg-chip--restricted { background: #2a1616; color: #e58b8b; border-color: #7a2f2f; }
/* C4 (S-D19): `own` is the fourth, most-specific source (tags used alongside the tags this file already
   carries) — its own violet border, declared FIRST of the source tokens so the others below still win only
   for their own token. */
.mg-chip--src-own { border-color: #6a4f8a; }
.mg-chip--src-related { border-color: #3f7a4f; }
.mg-chip--src-album { border-color: #7a6a2f; }
.mg-chip--src-sibling { border-color: #40707e; }
/* M96 G3: the one ranked list — ×N source-count badge, pending (dashed, awaiting lookup), folded-overflow. */
.mg-x { color: var(--muted); font-size: 11px; }
.mg-chip--pending { border-style: dashed; opacity: .85; }
.mg-chip--folded { display: none; }
/* C4 (S5, S-D11): a culled chip fades to nothing before its node is removed. The class sets the target
   opacity; the controller sets the transition duration inline from `fade_ms` so the config number wins. */
.mg-chip--culling { opacity: 0; pointer-events: none; }
/* C4 (S6, G-D2): the three bulk buttons row (Toggle all / Toggle off / Toggle), shown only with JS. */
.mg-tag-tools { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; position: relative; }
/* M145 (U-D3): the `⋯` "add a tag with a new id" button and its menu. The menu opens DOWNWARD and is
   anchored to the RIGHT of the tools row (the `⋯` is the row's last item, so the menu sits under it): its
   right edge is the tools-row right edge and its width is capped, so its left edge can never pass the panel
   left. It therefore stays inside the panel width at 400 px and on the desktop workbench, and causes no
   horizontal page scroll. The tools row is the positioned containing block. */
.mg-id-tag { display: inline-flex; }
.mg-id-tag-btn { line-height: 1; }
.mg-id-menu { list-style: none; margin: 2px 0 0; padding: 4px; position: absolute; top: 100%; right: 0; left: auto; z-index: 6; min-width: 160px; max-width: min(240px, calc(100% - 8px)); background: var(--panel); border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 6px 18px rgba(0, 0, 0, .28); }
.mg-id-menu[hidden] { display: none; }
.mg-id-menu li { margin: 0; }
.mg-id-menu-item { display: block; width: 100%; text-align: left; padding: 6px 10px; background: transparent; border: 0; border-radius: 6px; color: var(--text); cursor: pointer; font-size: 13px; text-transform: none; letter-spacing: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mg-id-menu-item:hover, .mg-id-menu-item:focus { background: var(--panel-2); outline: none; }
body.mode-phone .mg-id-menu-item { min-height: 32px; }
/* P4 / G-D5: the legend is a full-width flex row (space-between) — the label sits left, and a right-aligned
   META COLUMN holds the count with the status button UNDER it. The legend is at most two lines high. The
   count keeps text-transform:none / letter-spacing:0 so it does not inherit the uppercase legend style. */
.mg-tags > legend { display: flex; justify-content: space-between; align-items: flex-start; width: 100%; }
.mg-tags-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; }
.mg-tags-count { text-transform: none; letter-spacing: 0; color: var(--muted); font-size: 11px; }
.mg-more-tags { align-self: flex-start; }
/* G-D3: the status button — a small icon that shows the cull countdown / pause / lookup / failed state and
   acts as Refresh. Its icon is set per `data-mg-status`; the `counting` ring's duration + negative delay are
   written inline by the controller so a resumed countdown shows the part of the ring already used. */
.mg-tag-status { display: inline-flex; align-items: center; justify-content: center; padding: 0; width: 20px; height: 20px; background: transparent; border: 0; border-radius: 999px; color: var(--muted); cursor: pointer; text-transform: none; letter-spacing: 0; }
.mg-tag-status:hover { color: var(--text); }
.mg-status-icon { display: inline-block; width: 14px; height: 14px; box-sizing: border-box; }
/* idle: a refresh arrow (a ⟳ glyph keeps the CSS dependency-free). */
.mg-tag-status[data-mg-status="idle"] .mg-status-icon { width: auto; height: auto; }
.mg-tag-status[data-mg-status="idle"] .mg-status-icon::before { content: "\21BB"; font-size: 14px; line-height: 1; }
/* failed: a warning sign; the button also carries the accent so it stands out. */
.mg-tag-status[data-mg-status="failed"] { color: #e58b8b; }
.mg-tag-status[data-mg-status="failed"] .mg-status-icon { width: auto; height: auto; }
.mg-tag-status[data-mg-status="failed"] .mg-status-icon::before { content: "\26A0"; font-size: 14px; line-height: 1; }
/* paused: two bars. */
.mg-tag-status[data-mg-status="paused"] .mg-status-icon { border-left: 3px solid currentColor; border-right: 3px solid currentColor; width: 10px; height: 12px; }
/* loading: a spinning ring. counting: a ring that EMPTIES over idle_ms (its angle animates via @property). */
@property --mg-ring-deg { syntax: "<angle>"; inherits: false; initial-value: 360deg; }
.mg-tag-status[data-mg-status="loading"] .mg-status-icon,
.mg-tag-status[data-mg-status="counting"] .mg-status-icon { border-radius: 50%; }
.mg-tag-status[data-mg-status="loading"] .mg-status-icon { border: 2px solid var(--line); border-top-color: var(--accent); animation: mg-status-spin 0.8s linear infinite; }
.mg-tag-status[data-mg-status="counting"] .mg-status-icon { --mg-ring-deg: 360deg; background: conic-gradient(var(--accent) var(--mg-ring-deg), transparent 0deg); }
.mg-status-ring { animation-name: mg-ring-empty; animation-timing-function: linear; animation-iteration-count: 1; animation-fill-mode: forwards; }
@keyframes mg-status-spin { to { transform: rotate(360deg); } }
@keyframes mg-ring-empty { from { --mg-ring-deg: 360deg; } to { --mg-ring-deg: 0deg; } }
/* Reduced motion: neither ring moves; the icon is static and the title carries the state (G-D3). */
@media (prefers-reduced-motion: reduce) {
    .mg-tag-status[data-mg-status="loading"] .mg-status-icon,
    .mg-status-ring { animation: none !important; }
}
/* On a phone the status button is a 32 px touch target (G-D5). */
body.mode-phone .mg-tag-status { width: 32px; height: 32px; }
/* G-D6: the per-chip pencil edit button — OUTSIDE the label, so it never flips the checkbox or submits the
   form. A pencil on a fading chip is disabled. On a phone it grows to a 32 px touch target and the chip
   grows with it. A chip UNDER EDIT is dimmed with a dashed outline. */
.mg-chip-edit { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; padding: 0; margin-left: 2px; background: transparent; border: 0; border-radius: 4px; color: var(--muted); cursor: pointer; font-size: 11px; line-height: 1; }
.mg-chip-edit::before { content: "\270E"; }
.mg-chip-edit:hover { color: var(--text); }
.mg-chip-edit:disabled { opacity: .4; cursor: default; }
.mg-chip--editing { opacity: .6; outline: 1px dashed var(--accent); outline-offset: 1px; }
body.mode-phone .mg-chip { padding: 0 9px; min-height: 32px; }
body.mode-phone .mg-chip-edit { width: 32px; height: 32px; font-size: 13px; }
/* P5: the add field stays in view. It is sticky to the BOTTOM of the document scrollport (the page body,
   .mg-panel has no overflow), offset by the dock height so it comes to rest ABOVE the opaque action dock,
   never under it. --mg-dock-h is the shared offset (P-D6): the dock takes it as min-height and the field
   as bottom, so the two cannot drift. A sticky box is positioned, so the suggest menu still anchors to it.
   The panel background + top border keep the scrolling tag list from showing through. Its sticky range is
   the .mg-tags fieldset (its containing block), so it leaves with the fieldset. */
.mg-add { display: flex; flex-direction: column; gap: 4px; position: sticky; bottom: var(--mg-dock-h, 56px); z-index: 2; background: var(--panel); border-top: 1px solid var(--line); padding: 8px 0 2px; }
.mg-add-field { width: 100%; }
/* P5 / P-D7: the menu opens UPWARD (bottom:100%), because the stuck field rests at the bottom of the
   scrollport and a downward menu would open past the bottom edge where nothing can scroll it back. */
.mg-suggest-menu { list-style: none; margin: 0 0 2px; padding: 4px; position: absolute; top: auto; bottom: 100%; left: 0; right: 0; z-index: 5; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; max-height: 240px; overflow: auto; }
.mg-suggest-option { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; padding: 4px 8px; border-radius: 6px; cursor: pointer; font-size: 13px; }
.mg-suggest-name { margin-right: auto; }
.mg-suggest-option:hover, .mg-suggest-option.is-active { background: var(--panel-2); }
.mg-colourkey { font-size: 12px; }
.mg-colourkey-list { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.mg-key-swatch { display: inline-flex; align-items: center; padding: 2px 9px; border-radius: 999px; background: var(--panel-2); border: 1px solid var(--line); font-size: 12px; }
.mg-key-swatch--own { border-color: #6a4f8a; }
.mg-key-swatch--related { border-color: #3f7a4f; }
.mg-key-swatch--album { border-color: #7a6a2f; }
.mg-key-swatch--sibling { border-color: #40707e; }
/* M39 B3 sibling metadata block (below the last fieldset, above the sticky action row). */
.mg-siblings { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.mg-sibling-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; font-size: 12px; }
.mg-sibling-row > a, .mg-sibling-del > a { color: var(--accent); text-decoration: none; }
.mg-sibling-row > a:hover, .mg-sibling-del > a:hover { text-decoration: underline; }
.mg-sibling-tags { display: flex; flex-wrap: wrap; gap: 4px; }
/* K3: per-sibling delete checkbox + the "Delete selected duplicates" button (a formaction submit inside
   the one mg-form, so the sticky .mg-actions row stays its last child). */
.mg-sibling-del { display: inline-flex; align-items: baseline; gap: 6px; cursor: pointer; }
.mg-sibling-del input { width: auto; }
.mg-sibling-delete-btn { align-self: flex-start; margin-top: 4px; }
.mg-current-album { margin: 0; font-size: 13px; }
.mg-move-warn { margin: 0; font-size: 12px; }
.mg-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.mg-actions .btn { flex: 1 1 auto; text-align: center; }
/* The play action row pins to the viewport bottom while the play form is in view, so the buttons never
   scroll off (MG3). Scoped to .mg-form because done.html.twig reuses the bare .mg-actions class. The page
   body is the scrollport (.mg-panel has no overflow and no height), so a sticky bottom pins to the
   viewport. The opaque background + top border are required, or the fields scrolling under it show
   through. It keeps its DOM place, so tab order and the Delete confirm controller are unchanged. */
/* P-D6: --mg-dock-h is the ONE dock-height value. Declared on .mg-form (the common ancestor of the dock
   and the sticky .mg-add, so both inherit it), the dock takes it as min-height and .mg-add as its sticky
   bottom offset — one property, so the field's clearance and the real dock cannot drift apart. */
.mg-form { --mg-dock-h: 56px; }
.mg-form .mg-actions {
    position: sticky; bottom: 0; z-index: 1; min-height: var(--mg-dock-h);
    background: var(--bg); border-top: 1px solid var(--line); padding: 10px 0;
}
/* The Tags sheet (phone) is a modal <dialog>: its own scrollport, painted in the top layer ABOVE the
   dock, so the add field inside it needs no dock clearance. */
dialog.mg-sheet .mg-tags { --mg-dock-h: 0px; }

/* Done */
.mg-done { text-align: center; padding: 48px 0; }
.mg-done-ico { width: 40px; height: 40px; color: var(--accent); }
.mg-done .mg-actions { justify-content: center; }

/* -------- M96 G2 play: one template, two presentations (V52) ------------------------------------ */
/* The desktop workbench widens the whole admin shell (G-D9 a): every page that mounts minigame-layout —
   play, review and done — reads `body.mode-desktop`. */
body.mode-desktop .admin-shell { --admin-width: 1800px; }

/* No-JS / pre-connect default: one stacked column (the D4 fallback). */
.mg-play { display: block; }
.mg-form { display: flex; flex-direction: column; gap: 14px; }
.mg-stage { min-width: 0; border: 1px solid var(--line); border-radius: 10px; padding: 4px 16px 16px; }
.mg-stage .preview { max-width: none; margin: 0; }
/* Enter = Change & next: the first submit is a visually-hidden duplicate (clipped, not display:none). */
.mg-implicit-submit {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}
.mg-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.mg-bar-name { flex: 1 1 260px; min-width: 0; }
.mg-name { margin: 0; font-size: 1.4rem; }
.mg-name-input { width: 100%; font: inherit; font-size: inherit; font-weight: 700; background: var(--panel-2); color: var(--text); border: 1px solid var(--line); border-radius: 8px; padding: 4px 10px; }
.mg-bar-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; font-size: 12px; color: var(--muted); }
.mg-rule-label { max-width: 40ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mg-editor { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
.mg-lower-actions { display: flex; gap: 8px; }
.mg-more { position: relative; }
.mg-more > summary { list-style: none; cursor: pointer; }
.mg-more > summary::-webkit-details-marker { display: none; }
.mg-more-menu { display: flex; flex-direction: column; gap: 6px; position: absolute; right: 0; bottom: 100%; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 8px; z-index: 5; min-width: 180px; }
.mg-sheet { border: 1px solid var(--line); border-radius: 12px; background: var(--panel); color: var(--text); padding: 14px 16px; max-width: min(560px, 96vw); width: 96vw; }
.mg-sheet::backdrop { background: rgba(0,0,0,.5); }
.mg-sheet-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }

/* Desktop workbench: 1.75 : 1 media-to-editor. */
body.mode-desktop .mg-play { display: grid; grid-template-columns: 1.75fr 1fr; gap: 20px; align-items: start; }
body.mode-desktop .mg-stage { grid-column: 1; }
body.mode-desktop .mg-form { grid-column: 2; position: sticky; top: 16px; }
body.mode-desktop .mg-neighbour-cards { grid-column: 1 / -1; }
/* Phone-only pieces are hidden in the desktop default. */
.mg-lower, .mg-dock-phone-only { display: none; }
/* The sticky dock (.mg-form .mg-actions, bottom:0) floats over the scrollport. When the field column is
   only a little taller than the viewport, the last fields (album picker, source note) come to rest behind
   the opaque dock at max scroll and cannot be cleared. A bottom pad on the field column — placed BEFORE the
   dock — reserves a dock-tall gap so every field can scroll clear of it (M96 G2, "the dock never covers a
   field"). Padding AFTER the dock does not help. */
body.mode-phone .mg-lower { display: flex; flex-direction: column; gap: 10px; padding-bottom: 96px; }
body.mode-phone .mg-dock-phone-only { display: inline-flex; }
body.mode-phone .mg-dock-desktop-only, body.mode-phone .mg-desktop-only, body.mode-phone .mg-neighbour-cards { display: none; }
body.mode-phone .mg-editor:empty { display: none; }
/* Phone stage is edge to edge, black ground. */
body.mode-phone .mg-stage { border: 0; border-radius: 0; padding: 0; background: #000; }
body.mode-phone .mg-stage .preview-render img,
body.mode-phone .mg-stage video { object-fit: contain; }

/* Phone breakpoint (D4, 759px): the no-JS page stacks; the old 860px picker/play rule moves here. */
@media (max-width: 759px) {
    .mg-play { display: block; }
    .mg-bar-meta { font-size: 11px; }
}

/* ---- M40 near-duplicate + ban admin surface --------------------------------
   The NL/NA near-duplicate pages and the BN/NP ban pages shipped their markup
   without CSS; this block styles the class family they introduced (.notice*,
   .admin-tabs, .copy-btn, .btn-sm, .ndc-*, .nd-*, .ban-*). It reuses the same
   tokens and shapes as .dup-* / .badge / .pager. */

/* Shared bits */
.btn-sm { padding: 3px 9px; font-size: 12px; }
.copy-btn {
    margin-left: 4px; padding: 0 6px; font-size: 11px; line-height: 1.7;
    color: var(--muted); background: var(--panel-2); border: 1px solid var(--line);
    border-radius: 5px; cursor: pointer;
}
.copy-btn:hover { color: var(--text); border-color: var(--accent); }

.notice {
    margin: 0 0 14px; padding: 10px 12px; border: 1px solid var(--line);
    border-radius: 8px; background: var(--panel-2); color: var(--text); font-size: 13px;
}
.notice p { margin: 0 0 6px; }
.notice p:last-child { margin-bottom: 0; }
.notice ul { margin: 6px 0 0; padding-left: 20px; }
.notice-warn { border-color: #7a6a2f; background: rgba(122, 106, 47, .18); color: #e6d59a; }
.notice-error { border-color: #7a2f2f; background: rgba(122, 47, 47, .18); color: #e58b8b; }

.admin-tabs {
    display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 18px;
    border-bottom: 1px solid var(--line);
}
.admin-tabs a {
    padding: 7px 14px; color: var(--muted); font-size: 13px;
    border: 1px solid transparent; border-bottom: none; border-radius: 8px 8px 0 0;
}
.admin-tabs a:hover { color: var(--text); }
.admin-tabs a.is-active {
    color: var(--text); background: var(--panel); border-color: var(--line); margin-bottom: -1px;
}


/* NA suggestions page — one pair per row. M90 C4 (V12): every pair rule is scoped under `.ndc-page` so no
   minigame `.pair-card` style can leak in (and these never leak out). */
.ndc-page .ndc-row {
    display: flex; flex-wrap: nowrap; gap: 16px; align-items: flex-start; justify-content: space-between;
    padding: 14px; margin: 0 0 12px; background: var(--panel); border: 1px solid var(--line);
    border-radius: 10px;
}
/* goal D4: a phone is <= 759px — let the actions column drop below the pair and the sides stack. */
@media (max-width: 759px) {
    .ndc-page .ndc-row { flex-wrap: wrap; }
    .ndc-page .ndc-pair { flex-direction: column; }
    .ndc-page .ndc-actions { flex-direction: row; flex-wrap: wrap; width: 100%; }
    .ndc-page .ndc-actions form, .ndc-page .ndc-actions > .btn { flex: 1 1 auto; }
}
.ndc-page .ndc-pair { display: flex; flex-wrap: wrap; gap: 20px; flex: 1 1 auto; min-width: 0; }
.ndc-page .ndc-blob { display: flex; gap: 10px; min-width: 0; flex: 1 1 260px; }
.ndc-page .ndc-blob .blob-thumb { width: 96px; height: 96px; flex: none; object-fit: cover; border-radius: 8px; }
.ndc-page .ndc-blob .dup-thumb-placeholder { width: 96px; height: 96px; flex: none; }
.ndc-page .ndc-side--gone { opacity: .8; }
.ndc-page .ndc-side--gone .dup-thumb-placeholder { border: 1px dashed #7a3f3f; color: #c98a8a; }
.ndc-page .ndc-blob-meta { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.ndc-page .badge-removed { align-self: flex-start; background: rgba(122, 47, 47, .18); border: 1px solid #7a3f3f; color: #e58b8b; }
/* One item per line — a linked name plus its tags — instead of a run-on `·`-joined string. */
.ndc-page .ndc-items { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.ndc-page .ndc-items > li { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; font-size: 12px; min-width: 0; }
.ndc-page .ndc-item-name { color: var(--text); text-decoration: none; overflow-wrap: anywhere; }
.ndc-page .ndc-item-name:hover { text-decoration: underline; color: var(--accent); }
.ndc-page .ndc-actions { display: flex; flex-direction: column; align-items: stretch; gap: 8px; flex: none; width: 160px; }
.ndc-page .ndc-actions form { margin: 0; }
.ndc-page .ndc-actions .btn { display: block; width: 100%; text-align: center; }
.ndc-page .ndc-actions-spacer { border-top: 1px solid var(--line); margin: 2px 0; }
.ndc-page .ndc-distance { text-align: right; font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; margin-bottom: 2px; }

/* signature-verifier SV3: the three-case badge, shared by the list, the minigame and the confirm page.
   Colour is never the only signal — the label text is always present. The numbers wrap below the badge. */
.pair-case { display: inline-block; font-size: 12px; font-weight: 600; padding: 1px 6px; border-radius: 4px; border: 1px solid var(--border, #ccc); }
.pair-case--same { background: rgba(40, 150, 70, 0.14); border-color: rgba(40, 150, 70, 0.5); }
.pair-case--edit { background: rgba(200, 150, 20, 0.16); border-color: rgba(200, 150, 20, 0.5); }
.pair-case--uncertain { background: rgba(120, 120, 130, 0.14); border-color: rgba(120, 120, 130, 0.5); }
.pair-case-numbers { display: inline-block; font-size: 12px; font-variant-numeric: tabular-nums; margin-left: 6px; }
.ndc-case-filter { display: flex; flex-wrap: wrap; gap: 8px; margin: 6px 0; font-size: 13px; }
.ndc-case-filter a.is-active { font-weight: 700; text-decoration: underline; }
.ndc-ordered-note { font-size: 12px; color: var(--muted); margin: 2px 0 8px; }
@media (max-width: 640px) { .pair-case-numbers { display: block; margin-left: 0; } }

/* M74 F2: the directional derivation-pair badge — informational only, on the DERIVED side. */
.ndc-derivation-badge,
.ndm-derivation-badge {
    align-self: flex-start; display: inline-block;
    padding: 1px 8px; border-radius: 999px;
    background: #1d2b1f; color: #8fce9c; border: 1px solid #3f7a4f;
    font-size: 11px; font-weight: 600;
}
.ndm-derivation-badge { margin: 2px 0 6px; }

/* M50 near-duplicate REVIEW minigame — one pair at a time as a before/after slider + side panel */
.ndm-play { display: grid; grid-template-columns: minmax(0, 7fr) minmax(280px, 3fr); gap: 20px; align-items: start; }
@media (max-width: 759px) { .ndm-play { grid-template-columns: 1fr; } }
.ndm-meta { color: var(--muted); font-size: 13px; margin: 0 0 12px; }

/* M96 G6: the sibling review as the same screen kind as the tagging game. */
.ndm-stage { min-width: 0; }
.ndm-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.ndm-bar-title { margin: 0; font-size: 1.3rem; }
.ndm-badge { display: inline-flex; align-items: center; gap: .34em; padding: 2px 9px; border-radius: 999px; background: var(--panel-2); border: 1px solid var(--line); font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
/* C11 K1: the cards sit inside `.ndm-keeper`, BELOW the buttons, one per row so each is full width in
   the narrow panel. */
.ndm-sides { display: grid; grid-template-columns: 1fr; gap: 12px; margin-top: 4px; }
.ndm-keeper { display: flex; flex-direction: column; gap: 8px; }
.ndm-keeper-btns { display: flex; gap: 12px; }
/* C11 K2: the collapse hides the losing submit (`[data-nd-keeper][hidden]`); its whole column drops so
   the surviving column (flex: 1 1 0) widens to fill. The submit stays in the DOM (K-D5) — display:none
   comes from the `hidden` attribute, and the column drop keeps its stale consequence line out of view. */
.ndm-keeper-col:has(> button[data-nd-keeper][hidden]) { display: none; }
.ndm-keeper-col { flex: 1 1 0; display: flex; flex-direction: column; gap: 4px; }
.ndm-keeper-col .btn { width: 100%; }
.ndm-consequence { font-size: 12px; color: var(--muted); margin: 0; }
/* C6 (D1): the comparison line — one class on BOTH the card copy and the keeper-button copy (D-D7), so
   one rule sizes it and the phone rule can size the button copy further. */
.ndm-compare { display: block; font-size: 12px; color: var(--muted); margin: 2px 0 0; font-variant-numeric: tabular-nums; }
body.mode-phone .ndm-keeper-col .ndm-compare { font-size: 11px; }
/* The loser row is `.mg-dock-phone-only` (phone deck only). It must NOT set `display` unconditionally:
   that overrode `.mg-dock-phone-only { display: none }` (same specificity, defined later) and left the
   row — and its "Change ›" trigger — visible on DESKTOP, where clicking it opened the disposition
   <dialog> as a modal that is empty on desktop (the fieldset is re-homed inline), trapping focus until
   Esc. Layout is applied in phone mode only, so the phone-only hide/show machinery stays in control. */
.ndm-loser-row { font-size: 13px; margin: 0; align-items: center; gap: 8px; }
body.mode-phone .ndm-loser-row { display: flex; }
.ndm-dock { display: flex; flex-wrap: wrap; gap: 8px; }
.ndm-dock .btn { flex: 1 1 auto; text-align: center; }
/* The visible reason under the dock when "Album both" is refused (owner 2026-09-25). */
.ndm-dock-note { margin: 6px 0 0; display: flex; align-items: center; gap: 4px; }
/* C11 K2 (K-D13): on desktop the review panel becomes the SCROLL CONTAINER and the keeper buttons stick
   to its top, so the file cards (and the disposition, ban block and dock below them) scroll UNDER the
   buttons while the decision controls stay in view. Scoped to `.ndm-form` so the tagging minigame's
   `.mg-form` panel is untouched. `.ndm-form` is also `.mg-form`, already `position: sticky; top: 16px`
   (pinned to the document); the max-height + overflow makes it its own scrollport, and the nested sticky
   keeper-btns detaches inside it. On desktop the only absolutely-positioned overlays in the panel — the
   `⋯` more menu (`.mg-more-menu`) — are `.mg-dock-phone-only` (display:none here), and the inline-confirm
   block is in normal flow, so nothing clips: the K-D13 fallback is not needed. */
body.mode-desktop .ndm-form { max-height: calc(100dvh - 32px); overflow: auto; }
body.mode-desktop .ndm-keeper-btns { position: sticky; top: 0; z-index: 4; background: var(--bg); padding-bottom: 8px; border-bottom: 1px solid var(--line); }
/* Phone: a flex column of 100dvh so the keeper row and dock stay on screen while the frame blocks scroll.
   `align-items: stretch` is REQUIRED and is not the browser default here: `.ndm-play` above sets
   `align-items: start` for its desktop grid, that declaration is not overridden by this rule, and in a
   flex COLUMN the cross axis is horizontal — so `start` makes every child shrink-wrap its content width.
   `.ndm-frame.is-enhanced` has only absolutely positioned children, so its max-content width is 0: the
   stage collapsed to the 2px of its own borders and the pair rendered as a bare vertical line on phones
   (measured 2026-09-22 in headless Chrome at 400px: .ndm-stage 2px, .ndm-img 0px). The 400px sweep does
   not catch it — a zero-width box never scrolls the document sideways. */
body.mode-phone .ndm-play { display: flex; flex-direction: column; align-items: stretch; height: 100dvh; gap: 10px; }
body.mode-phone .ndm-stage { flex: 1 1 auto; min-height: 0; }
body.mode-phone .ndm-frame.is-enhanced { height: 100%; }
body.mode-phone .ndm-form { flex: 0 0 auto; }

/* The slider frame. No-JS default: layers stack vertically, both visible and labelled 1 / 2. The
   controller adds `is-enhanced` on connect(), which overlays them and reveals the divider. */
.ndm-frame {
    position: relative; min-width: 0; border: 1px solid var(--line); border-radius: 10px;
    background: var(--panel); overflow: hidden; user-select: none;
}
.ndm-frame.is-enhanced { height: min(70vh, 640px); touch-action: none; cursor: ew-resize; }
/* C11 K3 (K-D8): on the DESKTOP review page the frame takes its height from the VIEWPORT, so a portrait
   pair is read at full height instead of wasting most of a 640px box. The height is NOT drawn from the
   grid row (the desktop grid has no height of its own, so `align-items: stretch` would size the stage to
   the taller form column and grow/shrink with the disposition list) — it is `100dvh` minus the measured
   chrome above `.ndm-play` (112px at 1440x900) plus a 24px bottom gap = 136px, floored at 420px so the
   wipe never gets too small to judge. NO `align-items` is touched, so the SHARED `body.mode-desktop
   .mg-play` grid and the C1 phone override both stay exactly as they are. The GLOBAL rule above is KEPT
   as the frame's self-contained default (C10's W2 shares `.ndm-frame` with the wizard, which has no
   `.ndm-play`); this rule only overrides it on the review page in desktop mode. The phone rule below
   selects a mutually exclusive body class, so neither can override the other. */
body.mode-desktop .ndm-play .ndm-frame.is-enhanced { height: max(420px, calc(100dvh - 136px)); }
.ndm-layer { position: relative; display: block; min-width: 0; }
.ndm-frame.is-enhanced .ndm-layer { position: absolute; inset: 0; }
/* The bottom layer paints first, the top layer over it; the controller clips the top layer. */
.ndm-frame.is-enhanced .ndm-layer--top { z-index: 1; }
.ndm-img { display: block; width: 100%; height: auto; object-fit: contain; }
.ndm-frame.is-enhanced .ndm-img { width: 100%; height: 100%; }
.ndm-num {
    position: absolute; top: 8px; z-index: 2; min-width: 20px; padding: 1px 7px;
    border-radius: 999px; background: rgba(0,0,0,0.66); color: #fff; font-size: 12px; font-weight: 600;
    text-align: center;
}
/* When enhanced, the top layer (image 1) is wiped in from the LEFT and the bottom layer (image 2)
   shows on the RIGHT, so each badge sits over its own image — image 1 top-left, image 2 top-right.
   The controller also toggles `.ndm-num--off` from the committed divider, so a badge whose image is
   fully wiped away hides too (far-left shows only 2, far-right only 1). (Without JS both layers stack,
   both badges sit top-left, and neither is toggled off.) */
.ndm-num { left: 8px; }
.ndm-frame.is-enhanced .ndm-layer--bottom .ndm-num { left: auto; right: 8px; }
.ndm-num--off { display: none; }

/* The divider — hidden without JS, shown and draggable once enhanced. */
.ndm-divider { display: none; }
.ndm-frame.is-enhanced .ndm-divider {
    display: block; position: absolute; top: 0; bottom: 0; left: 50%; z-index: 3;
    width: 2px; margin-left: -1px; background: #fff; box-shadow: 0 0 0 1px rgba(0,0,0,0.4);
    cursor: ew-resize;
}
.ndm-divider:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ndm-handle {
    position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
    width: 28px; height: 28px; border-radius: 50%; background: #fff;
    box-shadow: 0 0 0 1px rgba(0,0,0,0.35), 0 1px 4px rgba(0,0,0,0.35);
}
.ndm-handle::before, .ndm-handle::after {
    content: ''; position: absolute; top: 50%; width: 0; height: 0;
    border-top: 5px solid transparent; border-bottom: 5px solid transparent;
}
.ndm-handle::before { left: 5px; border-right: 6px solid #444; }
.ndm-handle::after { right: 5px; border-left: 6px solid #444; }

/* The side panel — sticky, like the tagging minigame's. */
.ndm-panel { position: sticky; top: 16px; display: flex; flex-direction: column; gap: 14px; }
/* C11 K1/K-D11: a card is a `<button type="button">` selector, so it carries a button reset. Its content
   is phrasing-only (every former `<div>` is a `<span>` with block layout), a `<button>` permits no flow
   content and no nested interactive control. `aria-pressed` marks the chosen side. */
.ndm-side {
    display: block; width: 100%; font: inherit; text-align: left; color: inherit; appearance: none;
    border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; background: var(--panel);
    cursor: pointer; transition: border-color .12s, box-shadow .12s;
}
.ndm-side:hover { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.ndm-side:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ndm-side[aria-pressed="true"] { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }
.ndm-side-count { display: block; margin-top: 4px; }
.ndm-side-album { display: block; margin-top: 2px; }
.ndm-side-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
.ndm-side-num {
    display: inline-flex; align-items: center; justify-content: center; min-width: 20px; height: 20px;
    padding: 0 6px; border-radius: 999px; background: var(--accent); color: #fff; font-size: 12px; font-weight: 700;
}
.ndm-side-meta { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
.ndm-side-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
.ndm-side-tags .muted { font-size: 12px; }
.ndm-block { border: 0; margin: 0; padding: 0; }
.ndm-block legend { font-weight: 600; font-size: 13px; padding: 0; margin-bottom: 6px; }
.ndm-radio { display: flex; align-items: center; gap: 6px; font-size: 13px; margin-bottom: 3px; }
.ndm-hint { color: var(--muted); font-size: 12px; margin: 2px 0 0; }
.ndm-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.ndm-actions .btn { flex: 1 1 auto; text-align: center; }
.ndm-accept-row { display: flex; gap: 8px; }
.ndm-accept-row .btn { flex: 1 1 0; }

/* NL groups page — canonical + folded-hash table */
.nd-group-row {
    padding: 14px; margin: 0 0 14px; background: var(--panel); border: 1px solid var(--line);
    border-radius: 10px;
}
.nd-group-head { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 10px; }
.nd-group-head .blob-thumb,
.nd-group-head .dup-thumb-placeholder { width: 64px; height: 64px; flex: none; object-fit: cover; border-radius: 8px; }
.nd-group-meta { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 3px; }
.nd-group-head form { margin: 0; flex: none; }
.nd-dup-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.nd-dup-table th, .nd-dup-table td { text-align: left; padding: 6px 10px; border-bottom: 1px solid var(--line); vertical-align: middle; }
.nd-dup-table th { color: var(--muted); font-weight: 600; font-size: 12px; }
.nd-dup-table tr:last-child td { border-bottom: none; }
.nd-dup-table form { margin: 0; }

/* NL wizard — pick canonical + disposition */
.nd-form { display: flex; flex-direction: column; gap: 16px; }
.nd-groups, .nd-disposition, .nd-source {
    border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; margin: 0;
}
.nd-groups legend, .nd-disposition legend, .nd-source legend { padding: 0 6px; font-size: 12px; color: var(--muted); }
.nd-disposition label, .nd-source label { display: block; margin: 6px 0; }
.nd-disposition input[type="radio"], .nd-source input[type="radio"], .nd-item input[type="radio"] { width: auto; }
.nd-group { padding: 10px 0; border-bottom: 1px solid var(--line); }
.nd-group:last-child { border-bottom: none; padding-bottom: 0; }
.nd-items { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.nd-item {
    display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 6px 8px;
    background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px;
}
.nd-canonical { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; color: var(--muted); flex: none; }
.nd-item-name { font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
.nd-chain-note { font-style: italic; }
.nd-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
.nd-no-raster { font-style: italic; }
.nd-compare-btn { align-self: flex-start; margin-top: 2px; }
/* C10 W2 (W-D10): the wizard's compare container declares NO height of its own — it has no `.ndm-play`
   grid and no `.ndm-stage`, so the shared `.ndm-frame.is-enhanced` inside it takes its height from that
   rule's self-contained default (`min(70vh, 640px)`). This RELIES on C11 K3 keeping that default intact:
   K3 lets the review's `.ndm-play` grid override it, but the default must still resolve to a real height
   for a container that overrides nothing — otherwise the absolutely-positioned layers collapse to zero
   (the C1 phone-stage failure in a new page). The review and the wizard must be changed together. */
.nd-compare { display: block; margin: 0; }
.nd-compare > p { margin: 0 0 8px; }

/* NL confirm page — final review */
.nd-confirm {
    background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
    padding: 14px 16px; margin: 0 0 16px;
}
.nd-confirm h2 { font-size: 14px; margin: 14px 0 6px; color: var(--muted); }
.nd-confirm h2:first-child { margin-top: 0; }
.nd-confirm p { margin: 4px 0; }
.nd-confirm ul { margin: 4px 0 0; padding-left: 20px; }
.nd-apply { margin: 0; }

/* BN/NP ban page */
/* Dark-theme the ban note textarea — without this it inherits no control style and renders white. */
.cell-note textarea {
    padding: 6px 9px; background: var(--panel-2); color: var(--text);
    border: 1px solid var(--line); border-radius: 7px; font: inherit; font-size: 13px;
}
.facet--banned.is-active { border-color: #7a6a2f; color: #e6d59a; }
/* M93 A3: the proxy sweep sits in the toolbar row (its panel drops below the trigger). */
.ban-sweep { display: inline-flex; flex-direction: column; align-items: flex-start; gap: 8px; }
.cell-ban-kind { white-space: nowrap; }
.cell-note textarea { width: 100%; min-width: 180px; }
.cell-note .inline-form { margin: 0; }
.cell-retained .retained-tags { margin-top: 6px; }
.row-expansion__actions { display: flex; gap: 8px; margin-top: 8px; }
.admin-table tr.is-proxy-ban td:first-child { box-shadow: inset 3px 0 0 #6b5a2f; }

/* M93 A7: accordion mint/add panels and the client-side ignore path tester. */
.accordion-panel { flex-basis: 100%; margin-top: 8px; }
.new-pattern { display: inline-flex; flex-direction: column; align-items: flex-start; gap: 8px; }
.new-pattern .accordion-panel { flex-basis: auto; }
.ignore-tester-field { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.ignore-tester-field input[type="text"] {
    padding: 6px 9px; background: var(--panel-2); color: var(--text);
    border: 1px solid var(--line); border-radius: 7px; font: inherit; font-size: 13px; flex: 1 1 220px;
}
.ignore-test-status { }
.ignore-table tr.is-skip-match td { background: #2a1616; }

/* ---- version/5 release: other pages shipped without their CSS ---------------
   Beyond the near-dup/ban surface above, several buckets shipped new templates
   whose classes were never added here. This block styles the remaining real
   gaps: M37 api-keys mint panel, the M43 /random page, M40 preview/bulk ban
   surfacing, M33 delete-by-tag, and the M32/M39 facet + card-flag cues, plus the
   bare .warn utility used across the new confirm pages. */

/* Shared warning-paragraph utility (delete-by-tag, duplicates, play). */
.warn {
    margin: 0 0 12px; padding: 8px 12px; border: 1px solid #7a6a2f;
    border-radius: 8px; background: rgba(122, 106, 47, .18); color: #e6d59a; font-size: 13px;
}

/* M37 — API-key mint result (the one-time "copy this now" panel). */
.minted-key {
    margin: 0 0 18px; padding: 12px 14px; border: 1px solid #7a6a2f;
    border-radius: 10px; background: rgba(122, 106, 47, .16);
}
.minted-key-warning { margin: 0 0 8px; color: #e6d59a; font-size: 13px; }
.minted-key-label { margin: 0 0 6px; font-size: 13px; }
.minted-key-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.minted-key-value {
    flex: 1 1 260px; min-width: 0; padding: 6px 10px; border: 1px solid var(--line);
    border-radius: 7px; background: var(--panel-2); font-size: 13px; overflow-wrap: anywhere;
}

/* M43 — the /random page: search bar, preview frame, Next control. */
.random-search { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 0 0 18px; }
.random-search-input {
    flex: 1 1 260px; min-width: 0; padding: 8px 11px; background: var(--panel-2); color: var(--text);
    border: 1px solid var(--line); border-radius: 8px; font: inherit; font-size: 14px;
}
.random-search-btn { flex: none; }
.random-search-clear { flex: none; color: var(--muted); font-size: 13px; align-self: center; }
.random-search-clear:hover { color: var(--text); }
.random-empty { text-align: center; }

/* M40 — owner-only ban surfacing on the preview page and the bulk bar. */
.preview-ban-badge { margin: 10px 0 0; }
.nd-canonical-section { margin: 16px 0 0; padding-top: 12px; border-top: 1px solid var(--line); }
.ban-panel {
    display: flex; flex-direction: column; gap: 8px; margin: 8px 0; padding: 10px 12px;
    border: 1px solid #7a2f2f; border-radius: 8px; background: rgba(122, 47, 47, .14);
}
.ban-panel textarea { width: 100%; }
.ban-panel .ban-warn { margin: 0; color: #e58b8b; }
.ban-form { margin: 0; }

/* M33 — delete-items-by-tag preview + confirm. */
.delete-items-preview { margin: 12px 0 0; }
.delete-items-preview p { margin: 6px 0; }
.delete-items-confirm { margin: 16px 0 0; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.tag-delete-items { margin: 0; }

/* M39 — card-flag cues that shipped without a tint (their siblings set one). */
.card-flag--tags { background: rgba(52, 110, 74, .85); }
.card-flag--source { background: rgba(90, 60, 120, .85); }

/* Why a blob's thumbnail failed (admin blobs page), beside its `failed` state. */
.blob-fail-reason { max-width: 260px; margin-top: 2px; font-size: 11px; color: #d98c8c; overflow-wrap: anywhere; }
/* H5: the small "find first" file link beside a blob's Items count. */
.blob-file-link { margin-left: 6px; font-size: 11px; opacity: 0.75; }
.blob-file-link:hover { opacity: 1; }

/* Tag details page (M56): the header, the co-occurrence table, and the on-demand overlap sample. */
.tag-name-link { display: inline-block; margin-bottom: 4px; font-weight: 600; }
.tag-detail-head { margin: 4px 0 20px; }
.tag-detail-name { display: flex; align-items: center; gap: 10px; margin: 0; font-size: 20px; }
.tag-detail-sub { margin: 28px 0 8px; }
.tag-cooc-table td { vertical-align: middle; }
.tag-cooc-sample-row td { padding-top: 0; border-top: 0; }
.tag-cooc-sample:not([hidden]) { padding: 8px 0 4px; }
.card-grid--sample { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; }

/* version/8 M89 L1/L6: dashboard attention block, storage + media-health bars, recent-uploads strip. */
.attention { margin: 0 0 20px; }
.attention-empty { color: var(--muted); margin: 8px 0; }
.attention-list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 10px; }
.attention-row a {
    display: inline-flex; align-items: baseline; gap: 6px; padding: 8px 12px;
    background: var(--warn-bg); border: 1px solid var(--warn); border-radius: 8px; text-decoration: none;
}
.attention-count { font-weight: 700; color: var(--text); }
.attention-label { color: var(--muted); }

.usage, .media-health { margin: 0 0 20px; }
.usage-bar { display: flex; height: 14px; border-radius: 7px; overflow: hidden; background: var(--panel-2); }
.usage-seg { display: block; height: 100%; min-width: 1px; }
.usage-seg--content { background: var(--accent); }
.usage-seg--thumbs { background: #7ec8a0; }
.usage-seg--previews { background: #c8a07e; }
.usage-seg--db { background: #9a8fd0; }
.usage-seg--mh-ok { background: #2f6b3f; }
.usage-seg--mh-pending { background: #c8a07e; }
.usage-seg--mh-fail { background: #7a2f2f; }
.usage-legend { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 13px; }
.usage-legend a { text-decoration: none; color: inherit; }
.usage-legend a:hover { text-decoration: underline; }
.usage-percent { color: var(--muted); }
.usage-dot { display: inline-block; width: 9px; height: 9px; border-radius: 2px; margin-right: 4px; vertical-align: middle; }
.usage-dot--content { background: var(--accent); }
.usage-dot--thumbs { background: #7ec8a0; }
.usage-dot--previews { background: #c8a07e; }
.usage-dot--db { background: #9a8fd0; }
.usage-dot--mh-ok { background: #2f6b3f; }
.usage-note { margin: 6px 0 0; color: var(--muted); font-size: 13px; }
.usage-warn { color: #e39a9a; }
.media-health.is-stale { border-left: 3px solid var(--warn); padding-left: 10px; }

.recent { margin: 0 0 20px; }
.recent-title { font-size: 15px; margin: 0 0 10px; }
.recent-strip { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 12px; }
.recent-card { display: flex; flex-direction: column; gap: 4px; text-decoration: none; color: inherit; }
.recent-thumb { display: flex; align-items: center; justify-content: center; aspect-ratio: 1; background: var(--panel-2); border-radius: 8px; overflow: hidden; }
.recent-thumb img { width: 100%; height: 100%; object-fit: cover; }
.recent-thumb-none { color: var(--muted); font-size: 22px; }
.recent-name { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.recent-meta { display: flex; flex-wrap: wrap; gap: 6px; font-size: 12px; color: var(--muted); }

@media (max-width: 759px) {
    /* L-D3: the recent strip becomes a horizontal scroll row; the bars stack, no page-level scroll. */
    .recent-strip { display: flex; overflow-x: auto; -webkit-overflow-scrolling: touch; padding-bottom: 6px; }
    .recent-card { flex: 0 0 40vw; max-width: 160px; }
    .usage-legend { flex-direction: column; gap: 4px; }
}

/* ============================================================================
   version/9.3 unstyled-surface sweep. These rules give a rule to admin classes
   that shipped without one, so the elements render tidy and consistent with the
   existing admin design. Each block mirrors an existing pattern (.album-card,
   .bulk-bar, .inline-confirm, .badge, .nav-badge, .field, .notice). Tokens only.
   ============================================================================ */

/* ---- cross-cutting #1: .btn-small is an alias of the styled .btn-sm --------- */
.btn-small { padding: 3px 9px; font-size: 12px; }

/* ---- cross-cutting #2: the accent-toned inline confirm (mirrors --danger) --- */
.inline-confirm--primary { border-color: var(--accent); background: var(--surface-2, rgba(127,127,127,.16)); }

/* ---- cross-cutting #3: the missing album-badge enum modifiers. A hidden case
   reads muted like the base badge (only `--listed` is accent, `--hidden_restricted`
   red); naming each keeps the enum->class contract complete. */
.album-badge--hidden_untagged,
.album-badge--hidden_stale,
.album-badge--hidden_not_counted { color: var(--muted); border-color: var(--line); }

/* ---- an info-toned notice (sibling of .notice-warn / .notice-error) --------- */
.notice-info { border-color: var(--info); background: var(--info-bg); color: #8fbfe0; }

/* ---- a small state pill (mirrors .nav-badge) + a warning tone --------------- */
.pill {
    display: inline-flex; align-items: center; padding: 0 7px; border-radius: 999px;
    background: var(--panel-2); border: 1px solid var(--line); color: var(--muted);
    font-size: 11px; line-height: 1.7;
}
.pill--warn { border-color: #7a6a2f; color: #e6d59a; }

/* ---- a form-field error line (mirrors the .notice-error danger red) --------- */
.field-error { display: block; margin-top: 4px; color: #e58b8b; font-size: 12px; }

/* ============================ A) Collection merge wizard ==================== */
/* choose.html.twig — which collection survives: two radio-card columns. */
.merge-choose { margin: 0 0 20px; }
.merge-choose-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin: 0 0 16px; }
.merge-choose-col {
    display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; cursor: pointer;
    border: 1px solid var(--line); border-radius: 10px; background: var(--panel);
}
.merge-choose-col:has(input:checked) { border-color: var(--accent); }
.merge-choose-title { font-weight: 600; }
.merge-actions { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-top: 16px; }
.merge-cancel-form { margin-top: 24px; }

/* pair.html.twig — the two file columns, the no-JS add form, suggestions and the pair list. */
.merge-columns { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin: 0 0 16px; }
.merge-col {
    display: flex; flex-direction: column; gap: 10px; padding: 12px;
    border: 1px solid var(--line); border-radius: 10px; background: var(--panel);
}
.merge-col h3 { margin: 0; font-size: 15px; }
.merge-pick { padding: 6px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); cursor: pointer; }
.merge-pick:hover { border-color: var(--accent); }
.merge-pair-error { min-height: 1em; margin: 8px 0; color: #e58b8b; font-size: 13px; }
.merge-pair-nojs { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px; margin: 0 0 16px; }
.merge-pair-nojs label { display: flex; flex-direction: column; gap: 4px; color: var(--muted); font-size: 13px; }
.merge-pair-nojs select { padding: 6px 9px; background: var(--panel-2); color: var(--text); border: 1px solid var(--line); border-radius: 7px; font: inherit; font-size: 13px; }
.merge-suggestions, .merge-pair-list {
    padding: 12px 14px; margin: 0 0 16px;
    border: 1px solid var(--line); border-radius: 10px; background: var(--panel);
}
.merge-suggestions h3, .merge-pair-list h3 { margin: 0 0 10px; font-size: 15px; }
.merge-suggestion-list { list-style: none; margin: 10px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.merge-suggestion, .merge-pair-row {
    display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
    padding: 8px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2);
}
.merge-pair-row { margin: 0 0 8px; }
.merge-pair-num { flex: none; min-width: 1.8em; text-align: center; color: var(--muted); font-variant-numeric: tabular-nums; }

/* _box.html.twig — one small gallery tile (media + caption). The _card_media still is sized here. */
.merge-box { margin: 0; width: 96px; display: flex; flex-direction: column; gap: 4px; }
.merge-box .card-still, .merge-box > img { width: 96px; height: 96px; object-fit: cover; border-radius: 6px; background: var(--panel-2); display: block; }
.merge-box .card-motion { display: none; }
.merge-box svg { width: 40px; height: 40px; color: var(--muted); }
.merge-box-name { font-size: 11px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* _decision_row.html.twig — one pair's decision fieldset. */
.merge-decision-row { padding: 12px 14px; margin: 0 0 14px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); }
.merge-decision-row legend { color: var(--muted); font-size: 13px; padding: 0 4px; }
.merge-decision-boxes { display: flex; flex-wrap: wrap; gap: 10px; margin: 0 0 10px; }
.merge-decision-choices { display: flex; flex-direction: column; gap: 6px; margin: 0 0 8px; }
.merge-decision-choices label { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-size: 14px; }
.merge-decision-keeper { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 0 0 8px; }
.merge-decision-keeper label { display: inline-flex; align-items: center; gap: 4px; font-size: 13px; }
.merge-decision-source { display: flex; flex-direction: column; gap: 4px; max-width: 360px; color: var(--muted); font-size: 13px; }
.merge-decision-source select { padding: 6px 9px; background: var(--panel-2); color: var(--text); border: 1px solid var(--line); border-radius: 7px; font: inherit; }
.merge-decision-outcome { margin: 6px 0 0; font-size: 13px; }

/* decide.html.twig / order.html.twig / result.html.twig */
.merge-decide-form { margin: 0 0 16px; }
.merge-order-form { margin: 0 0 20px; }
.merge-order-choices { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0 0 16px; }
.merge-order-choices label { display: inline-flex; align-items: center; gap: 6px; font-size: 14px; }
.merge-order-preview { padding: 12px 14px; margin: 0 0 16px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); }
.merge-order-preview h3 { margin: 0 0 10px; font-size: 15px; }
.merge-order-preview-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.merge-result-counts { list-style: none; margin: 0 0 16px; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 6px; }
.merge-result-counts li { padding: 6px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); font-size: 13px; }
.merge-refused { padding: 12px 14px; margin: 16px 0; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); }
.merge-refused h3 { margin: 0 0 10px; font-size: 15px; }

/* ============================ B) Collections list / grid cards ============== */
/* albums/index.html.twig grid + CollectionCard.html.twig — mirror .album-grid / .album-card. */
.collections-form { margin: 0; }
.collection-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 14px; margin: 0 0 20px; }
.collection-card {
    position: relative; display: flex; flex-direction: column; overflow: hidden;
    border: 1px solid var(--line); border-radius: 10px; background: var(--panel);
}
.collection-card:hover { border-color: var(--accent); }
.collection-select {
    position: absolute; top: 6px; left: 6px; z-index: 1; display: inline-flex;
    padding: 2px 4px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel);
}
.collection-card-cover { aspect-ratio: 4 / 3; background: var(--panel-2); display: flex; align-items: center; justify-content: center; overflow: hidden; }
.collection-card-cover img, .collection-cover-thumb { width: 100%; height: 100%; object-fit: cover; }
.collection-card-noimg { color: var(--muted); display: inline-flex; }
.collection-card-body { padding: 8px 10px; display: flex; flex-direction: column; gap: 4px; }
.collection-card-title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: inherit; text-decoration: none; }
.collection-card-title:hover { text-decoration: underline; }
.collection-card-attribution { display: flex; flex-wrap: wrap; gap: 2px 8px; }
.collection-card-host { font-variant: small-caps; }
.collection-card-note { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.collection-card-tags { display: flex; flex-wrap: wrap; gap: 4px; font-size: 12px; }
.collection-tag {
    display: inline-flex; align-items: baseline; gap: 4px; padding: 1px 7px;
    border: 1px solid var(--line); border-radius: 999px; background: var(--panel-2);
    color: var(--muted); text-decoration: none;
}
.collection-tag:hover { border-color: var(--accent); color: var(--text); text-decoration: none; }
.collection-card-more { font-size: 12px; }
.collection-card-more > summary { cursor: pointer; color: var(--muted); }
/* the list view uses its own table class (not .admin-table) — mirror .admin-table. */
.collections-table { width: 100%; border-collapse: collapse; margin: 8px 0 24px; }
.collections-table th, .collections-table td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
.collections-table th { color: var(--muted); font-weight: 600; font-size: 13px; }
.col-cover { width: 1%; white-space: nowrap; }
.collections-table .collection-cover-thumb { width: 56px; height: 42px; border-radius: 6px; border: 1px solid var(--line); display: block; }

/* ============================ C) Tag-rule form + simulate =================== */
/* tag_rules/form.html.twig — the one source text area and the Simulate preview. */
.tag-rule-source { width: 100%; resize: vertical; font-family: var(--mono, monospace); font-size: 13px; }
.tag-rule-source.is-invalid { border-color: #e58b8b; }
.tag-rule-errors { margin: 6px 0 0; padding: 0; list-style: none; }
.tag-rule-errors li + li { margin-top: 6px; }
.tag-rule-error-text { display: block; margin-top: 2px; padding: 4px 6px; border-radius: 6px; background: var(--panel); color: var(--text, inherit); white-space: pre-wrap; overflow-wrap: anywhere; font-size: 12px; }
.tag-rule-error-text mark { background: #7a2f2f; color: #fff; border-radius: 3px; padding: 0 2px; }
.tag-rule-diagnostics { margin: 6px 0 0; padding: 0; list-style: none; }
.tag-rule-diagnostics li + li { margin-top: 4px; }
.tag-rule-diagnostic code { overflow-wrap: anywhere; }
.tag-rule-diagnostic--error { color: #e58b8b; }
.tag-rule-diagnostic--warn { color: #e6d59a; }
.tag-rule-diagnostic--info { color: var(--muted); }
.tag-rule-simulate { margin: 18px 0 0; padding: 12px 14px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); }
.tag-rule-simulate h2 { margin: 0 0 8px; font-size: 16px; }
.sim-buckets { list-style: none; margin: 0 0 8px; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.sim-buckets li { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.sim-buckets strong { font-variant-numeric: tabular-nums; }
.sim-contradictions { margin: 8px 0; color: var(--warn); }
.sim-sample { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
.sim-sample-item { font-size: 12px; }

/* ============================ D) Store inventory ============================ */
/* store_inventory.html.twig — toolbar, note, section headings, corrupt badge.
   The three tables already carry .admin-table (+ --cards); the *-mime/-video/-uncovered
   hooks need no rule of their own. */
.inventory-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; margin: 0 0 16px; }
.admin-note { margin: 0 0 14px; padding: 8px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); color: var(--muted); font-size: 13px; }
.admin-subtitle { margin: 22px 0 6px; font-size: 16px; }
.badge-warn { border-color: #7a6a2f; color: #e6d59a; }

/* ============================ E) Tags health =============================== */
/* tags_health.html.twig — the rule tiles and the per-row break / allow-word bits. */
.health-tiles { display: flex; flex-wrap: wrap; gap: 10px; margin: 0 0 18px; }
.tile {
    display: flex; flex-direction: column; gap: 2px; min-width: 110px; padding: 10px 12px;
    border: 1px solid var(--line); border-radius: 10px; background: var(--panel);
    color: inherit; text-decoration: none;
}
.tile:hover { border-color: var(--accent); text-decoration: none; }
.tile.is-active { border-color: var(--accent); background: var(--panel-2); }
.tile-label { color: var(--muted); font-size: 12px; }
.tile-count { font-size: 20px; font-weight: 600; font-variant-numeric: tabular-nums; }
.health-break { margin: 0 0 4px; font-size: 13px; }
.health-words { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
.inline-allow { display: inline-flex; align-items: center; gap: 4px; margin: 0; }

/* ============================ F) Tag-rules index =========================== */
/* tag_rules/index.html.twig — the contradictions panel and the per-row actions cell. */
.tag-rules-contradictions { padding: 12px 14px; margin: 0 0 16px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); }
.tag-rules-contradictions h2 { margin: 0 0 10px; font-size: 16px; }
.tag-rules-actions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }

/* ============================ G) Ban page ================================== */
/* ban.html.twig + _partials/ban_targets.html.twig */
.ban-hash-entry { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px; margin: 0 0 16px; }
.ban-hash-entry label { width: 100%; color: var(--muted); font-size: 13px; }
.ban-hash-entry input { flex: 1 1 320px; min-width: 0; padding: 8px 10px; background: var(--panel-2); color: var(--text); border: 1px solid var(--line); border-radius: 7px; font: inherit; }
.ban-view { display: flex; flex-direction: column; gap: 14px; }
.ban-target { display: flex; gap: 12px; align-items: flex-start; padding: 12px 14px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); }
.ban-target__name { font-weight: 600; overflow-wrap: anywhere; }
.ban-summary { margin: 0; padding: 10px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); font-size: 14px; }
.ban-apply { margin: 0; display: flex; flex-direction: column; gap: 12px; }

/* ============================ H) Bulk bar extras =========================== */
/* _partials/bulk_bar.html.twig — the panels' display is owned by the field-switch
   controller (the `hidden` attribute), so these style CONTENT only, never display. */
.bulk-alert { display: block; margin: 0 0 6px; color: #e58b8b; font-size: 13px; }
.bulk-note { color: var(--muted); font-size: 13px; }
.bulk-replace-pair { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.bulk-replace-arrow { color: var(--muted); }
.bulk-panel--share input[type="number"] { width: 5em; padding: 6px 9px; background: var(--panel-2); color: var(--text); border: 1px solid var(--line); border-radius: 7px; font: inherit; font-size: 13px; }
.bulk-message { margin: 10px 0; padding: 8px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); color: var(--text); font-size: 13px; }

/* ============================ I) Variants show ============================= */
/* variants/show.html.twig — the member list (thumb + name + size + actions). */
.batch-form { margin: 0 0 24px; }
.variant-members { list-style: none; margin: 0 0 16px; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.variant-member { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); }
.variant-member.is-primary { border-color: var(--accent); }
.variant-member-actions { margin-left: auto; display: inline-flex; flex-wrap: wrap; gap: 6px; align-items: center; }

/* ============================ J) Banned reasons ============================ */
/* banned_reasons.html.twig — the table carries .admin-table; style the links/overflow. */
.reason-link { overflow-wrap: anywhere; }
.reason-toolong { color: var(--muted); overflow-wrap: anywhere; }

/* ============================ K) Owner controls =========================== */
/* file/preview/_owner_controls.html.twig — variant / origin / borrow blocks. */
.owner-variant { display: flex; flex-direction: column; gap: 8px; margin: 12px 0; }
.owner-variant-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.owner-origin { display: inline-flex; align-items: center; gap: 6px; margin: 8px 0; }
.owner-borrow { display: flex; flex-direction: column; gap: 8px; margin: 12px 0; padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel-2); }
.owner-borrow-status { display: inline-flex; align-items: center; gap: 6px; margin: 0; }

/* ============================ L) Collection show =========================== */
/* albums/show.html.twig — reverse-order form + the markdown attribution note. */
.album-reverse-form { margin: 0 0 16px; }
.collection-attribution-note { max-width: 560px; margin: 4px 0 8px; color: var(--muted); line-height: 1.6; }
.collection-attribution-note > :first-child { margin-top: 0; }
.collection-attribution-note > :last-child { margin-bottom: 0; }

/* ============================ M) Minor spacing ============================= */
.variant-suppression-note { margin: 6px 0 0; }
.ndm-revert-line { margin-top: 4px; }

/* ---- phone (admin breakpoint): collapse the merge/collection grids to one
   column and let the variant member wrap, so nothing scrolls sideways. */
@media (max-width: 759px) {
    .merge-choose-grid, .merge-columns { grid-template-columns: 1fr; }
    .collection-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
    .variant-member { flex-wrap: wrap; }
    .variant-member-actions { margin-left: 0; }
    .ban-target { flex-wrap: wrap; }
}
