/**
 * Omnibus Tracker — Dark Collector's Shelf theme.
 * Premium dark editorial design for omnibus edition tracking.
 */

/* ============================================================
   1. Custom Properties
   ============================================================ */
:root {
    /* The tile's own tokens (--omni-surface/-border/-text/-text-muted/
       -radius/-radius-sm) live in comic_tile.css, which this sheet's
       non-tile rules also reference. Any page loading omnis.css MUST
       load comic_tile.css too (pinned by tests/test_sales_page.py). */
    --omni-bg: var(--bg);
    --omni-surface-hover: var(--surface-2);
    --omni-text-dim: var(--text-dim);
    --omni-accent-upcoming: var(--accent-upcoming);
    --omni-accent-recent: var(--accent-recent);
    --omni-accent-mine: var(--accent-interest);
    --omni-accent-mine-soft: var(--accent-interest);
    --omni-accent-edition-type: var(--brand);
    --omni-radius-lg: 12px;
    --omni-radius-pill: 50px;
    --omni-font-display: var(--font-display);
    --omni-font-body: var(--font-body);
    --omni-glass-bg: var(--glass-bg);
    --omni-glass-border: var(--glass-border);
    /* The page's one content width, shared by the header and every wide
       block below it (.omni-shell). 1400px holds six 200px grid columns, so
       a laptop loses nothing and only ultrawides are reined in; the other
       browse surfaces cap at 1200px (/search results, this page's calendar)
       and a Bootstrap .container (/sales). Declared ONCE on purpose — the
       drift between an 1100px header and an uncapped grid is what this
       replaces, and tests/test_omnis_content_shell.py pins the count. */
    --omni-page-max: 1400px;
}

/* ============================================================
   2. Dark Body (grain overlay — dark-only flourish)
   ============================================================ */
[data-theme="dark"] body::after {
    content: '';
    position: fixed;
    /* iOS 26 Safari ("Liquid Glass") tints the status/toolbar by SAMPLING the
       background of position:fixed/sticky elements touching the viewport edges
       (pseudo-elements of the sampled element are skipped, but a full-viewport
       fixed overlay like this IS sampled). A bare `inset: 0` puts this nearly
       transparent grain over the top edge, so Safari samples it (seeing the
       cover art behind) instead of falling back to the solid body background —
       the status bar bleeds on /omnis and /shelf. Inset off the top (navbar
       already bakes in safe-area-inset-top) and bottom sampled strips so Safari
       falls back to the solid body bg there. env() is 0 off-iOS → no-op. */
    inset: var(--nav-h) 0 env(safe-area-inset-bottom, 0px) 0;
    pointer-events: none;
    opacity: 0.03;
    background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
    z-index: 9999;
}

/* ============================================================
   4. Hero Header
   ============================================================ */

/* ============================================================
   5. Editorial Header — Tabs + Toolbar
   ============================================================ */
/* One content edge for the whole page. The toolbar and the grid it controls
   read the SAME cap and the SAME gutter — both boxes are border-box, so the
   padding is part of the edge and a shared max-width alone would still leave
   them misaligned. Previously the header was capped at 1100px while the grid
   below sat full-bleed in `.container-fluid`, which on a wide display put the
   controls in a narrow centred column floating over a grid that ran past them
   on both sides. Every mobile clamp below must move BOTH selectors.

   The masthead is capped by WRAPPING it in `.omni-shell` (see omnis.html),
   not by re-declaring `.pm-head` here: that is a shared primitive owned by
   page_masthead.css, and a second owner fails
   tests/test_css_shared_primitives.py. Capping the content while leaving the
   title full-bleed just trades one misalignment for another. */
.omni-header,
.omni-shell {
    max-width: var(--omni-page-max);
    margin-left: auto;
    margin-right: auto;
    padding: 0 1.25rem;
}

.omni-header {
    margin-bottom: 1.75rem;
}

/* Featured publisher controls stay together within the secondary toolbar. */
.omni-publisher-group {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem;
    flex: 0 1 auto;
}
.omni-pub-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    min-height: 40px;
    flex: none;
}
/* Native disclosure and checkboxes keep the full scoped directory keyboard accessible. */
.omni-publisher-picker { position: relative; max-width: 100%; }
.omni-publisher-picker summary { cursor: pointer; list-style: none; }
.omni-publisher-picker summary::-webkit-details-marker { display: none; }
.omni-publisher-directory {
    position: absolute;
    z-index: 25;
    top: calc(100% + 0.5rem);
    right: 0;
    width: 22rem;
    max-width: calc(100vw - 3rem);
    border-color: var(--border-strong);
    border-radius: var(--radius-sm);
    box-shadow: none;
}
.omni-publisher-search-label { display: block; margin-bottom: 0.35rem; }
.omni-publisher-directory input[type="search"] {
    width: 100%;
    min-height: 44px;
    padding: 0.5rem;
    background: var(--bg);
    color: var(--text);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    caret-color: var(--brand);
}
.omni-publisher-directory input::placeholder { color: var(--text-muted); }
.omni-publisher-hint { color: var(--text-muted); font-size: 0.875rem; margin: 0.5rem 0; }
.omni-publisher-options { border: 0; padding: 0; margin: 0 0 0.75rem; max-height: 14rem; overflow-y: auto; }
.omni-publisher-option { display: flex; align-items: center; gap: 0.6rem; min-height: 44px; cursor: pointer; overflow-wrap: anywhere; }
.omni-publisher-option input { accent-color: var(--brand); flex: none; width: 1.1rem; height: 1.1rem; }
.omni-publisher-option:hover { background: var(--surface-2); }
.omni-publisher-picker summary:focus-visible,
.omni-publisher-directory :focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; }
@media (max-width: 767.98px) {
    .omni-publisher-picker[open] { width: 100%; }
    .omni-publisher-directory { position: static; width: 100%; max-width: 100%; margin-top: 0.5rem; }
    .omni-publisher-options { max-height: 10rem; }
}
/* Active tinted chip: the pub-tint gradient + pub-ring outline carry the
   selected state; suppress the generic brand fill so the tint reads. */
.omni-pub-chip.is-active {
    background-color: var(--surface);
    color: var(--text);
    border-color: transparent;
}
/* The text-only chips ("Major publishers", "All publishers") have no tint to
   carry the pressed state, so with the border dropped above they read as
   barely different from released. The ring is what tells a reader which
   publisher scope is in force (GH-1089). */
.omni-pub-chip.is-active:not(.pub-tint) {
    box-shadow: inset 0 0 0 2px var(--brand);
}
.omni-pub-logo { height: 22px; width: auto; display: block; }

/* ---- Tier 1: Mode tabs ---- */
.omni-tabs {
    display: flex;
    justify-content: center;
    gap: 2.75rem;
    border-bottom: 1px solid var(--border);
    margin-bottom: 0.95rem;
}

.omni-tab {
    position: relative;
    background: transparent;
    border: 0;
    color: var(--omni-text-muted);
    font-family: var(--omni-font-display);
    font-size: 1.18rem;
    letter-spacing: 0.005em;
    padding: 0.85rem 0.35rem 1.05rem;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    line-height: 1;
    transition: color 0.22s ease;
}

.omni-tab:hover {
    color: var(--omni-text);
}

.omni-tab.is-active {
    color: var(--omni-text);
}

/* The animated underline is now the shared .cc-tabs__ind slider (cc_tabs.css).
   Pipe the active mode's accent into it via --cc-tab-accent, and keep the
   active tab's text in that accent too. */
.mode-upcoming .omni-tabs { --cc-tab-accent: var(--omni-accent-upcoming); }
.mode-recent   .omni-tabs { --cc-tab-accent: var(--omni-accent-recent); }
.mode-mine     .omni-tabs { --cc-tab-accent: var(--omni-accent-mine); }
.mode-upcoming .omni-tab.is-active { color: var(--omni-accent-upcoming); }
.mode-recent   .omni-tab.is-active { color: var(--omni-accent-recent); }
.mode-mine     .omni-tab.is-active { color: var(--omni-accent-mine); }

/* Inactive: white. Active: accent color (used by underline). Label stays
   white-ish even when accent is on — the underline carries the color. */
.omni-tab.is-active .omni-tab-label { color: var(--omni-text); }

.omni-tab-label { display: inline-block; }
/* Responsive label pair for the "Recently Released" tab (see omnis.html).
   Desktop shows the long string; the <=640px block swaps them. */
.omni-tab-label-short { display: none; }

/* Tiny refined number: typographic, not chip-style */
.omni-tab-count {
    font-family: var(--omni-font-body);
    font-size: 0.7rem;
    font-weight: 500;
    letter-spacing: 0.06em;
    color: var(--omni-text-dim);
    padding-left: 0.55rem;
    margin-left: 0.1rem;
    border-left: 1px solid color-mix(in srgb, var(--text) 12%, transparent);
    line-height: 1;
    transform: translateY(-1px);
}
.omni-tab-count[data-omni-count-placeholder] {
    display: inline-block;
    min-width: 2.3ch;
    min-height: 1em;
}
.omni-tab.is-active .omni-tab-count {
    color: currentColor;
    border-left-color: currentColor;
    opacity: 0.7;
}

/* My List tab keeps the scarlet heart inline as a personality cue */
.omni-tab-mine .omni-tab-heart {
    width: 13px;
    height: 13px;
    stroke: var(--omni-accent-mine-soft);
    fill: var(--omni-accent-mine-soft);
    stroke-width: 1.5;
    stroke-linejoin: round;
    transition: transform 0.22s ease, fill 0.22s ease, stroke 0.22s ease;
}
.omni-tab-mine:hover .omni-tab-heart {
    transform: scale(1.12);
}

/* ---- Tier 2: Toolbar ---- */
.omni-toolbar {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

/* ---- Tier 1.5: search row ----
   Search was previously wedged mid-toolbar between the paperbacks switch and
   the publisher chips — the weakest available position for the page's primary
   affordance. Its own row above the controls gives it the hierarchy it earns,
   at both widths. */
.omni-searchbar {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin-bottom: 0.75rem;
}

.omni-searchbar .omni-search {
    flex: 1 1 auto;
    min-width: 0;
}

/* Trigger sits at the right edge of the row. Search is the primary affordance
   and takes the left, matching DOM order — nothing here may set `order` on
   either child. A leftover `.omni-search { order: 10 }` from the pre-rework
   layout (when search was its own full-width row below the toolbar) used to
   sort the search AFTER the trigger and flipped the whole row; it is deleted,
   and tests/test_omnis_filter_sheet.py pins that no rule reintroduces it. */
.omni-searchbar .omni-filter-trigger {
    flex: 0 0 auto;
    margin-left: auto;
}

/* ---- Active filter chips (mobile only) ----
   Desktop already shows every filter control in the toolbar, so a chip row
   there would restate what is on screen. Hidden by default; the mobile block
   below turns it on. */
.omni-active-filters { display: none; }

/* ---- Bottom-sheet chrome ----
   Present in the DOM at every width but only meaningful below 640px, where
   .omni-toolbar becomes a fixed sheet. Above that the toolbar is a plain
   inline row and this chrome would be noise. */
.omni-sheet-backdrop,
.omni-sheet-head,
.omni-sheet-foot,
.omni-controls-label { display: none; }

.omni-active-filter {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    min-height: 34px;
    padding: 0.3rem 0.55rem 0.3rem 0.7rem;
    font-size: 0.78rem;
    background: color-mix(in srgb, var(--text) 8%, transparent);
    border-color: color-mix(in srgb, var(--text) 18%, transparent);
    color: var(--omni-text);
}

/* Content scope widens rather than narrows. The dashed edge plus the '+ '
   prefix in the label keep the row from implying it is filtering anything
   out. */
.omni-active-filter.is-expansion {
    border-style: dashed;
    color: var(--omni-text-muted);
}

.omni-active-filter-x {
    font-size: 0.95rem;
    line-height: 1;
    opacity: 0.7;
}
.omni-active-filter:hover .omni-active-filter-x { opacity: 1; }

/* Ghost toggle buttons: low-noise text options */
.omni-ghost {
    background: transparent;
    border: 1px solid transparent;
    color: var(--omni-text-dim);
    font-family: var(--omni-font-body);
    font-size: 0.78rem;
    font-weight: 500;
    letter-spacing: 0.02em;
    padding: 0.4rem 0.85rem;
    border-radius: var(--omni-radius-pill);
    cursor: pointer;
    transition: color 0.18s ease, background 0.18s ease, border-color 0.18s ease;
    white-space: nowrap;
}
.omni-ghost:hover {
    color: var(--omni-text-muted);
    background: color-mix(in srgb, var(--text) 4%, transparent);
}
.omni-ghost.active {
    color: var(--omni-text);
    background: color-mix(in srgb, var(--text) 10%, transparent);
    border-color: var(--omni-glass-border);
}

.omni-paperbacks-switch {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    min-height: 30px;
    padding: 0.25rem 0.3rem 0.25rem 0.65rem;
    border: 1px solid var(--omni-glass-border);
    border-radius: var(--omni-radius-pill);
    background: color-mix(in srgb, var(--text) 3%, transparent);
    color: var(--omni-text-dim);
    font-family: var(--omni-font-body);
    font-size: 0.78rem;
    font-weight: 500;
    letter-spacing: 0.02em;
    cursor: pointer;
    white-space: nowrap;
    transition: color 0.18s ease, background 0.18s ease, border-color 0.18s ease;
}

.omni-paperbacks-switch:hover {
    color: var(--omni-text-muted);
    border-color: color-mix(in srgb, var(--text) 22%, transparent);
}

.omni-paperbacks-switch.is-active {
    color: var(--omni-text);
    background: color-mix(in srgb, var(--text) 7%, transparent);
    border-color: color-mix(in srgb, var(--text) 26%, transparent);
}

.omni-switch-track {
    width: 34px;
    height: 18px;
    padding: 2px;
    border-radius: var(--omni-radius-pill);
    background: color-mix(in srgb, var(--text) 11%, transparent);
    display: inline-flex;
    align-items: center;
    flex: 0 0 auto;
    transition: background 0.18s ease;
}

.omni-switch-thumb {
    width: 14px;
    height: 14px;
    border-radius: 999px;
    background: var(--omni-text-muted);
    box-shadow: 0 1px 3px color-mix(in srgb, var(--shadow-ink) 28%, transparent);
    transform: translateX(0);
    transition: transform 0.18s ease, background 0.18s ease;
}

.omni-paperbacks-switch.is-active .omni-switch-track {
    background: var(--omni-accent-edition-type);
}

.omni-paperbacks-switch.is-active .omni-switch-thumb {
    background: var(--shadow-ink);
    transform: translateX(16px);
}

.omni-switch-label {
    line-height: 1;
}

.omni-switch-label-short {
    display: none;
}

/* Filters trigger — a hair more visual weight than ghost buttons */
.omni-filter-trigger {
    background: transparent;
    border: 1px solid var(--omni-glass-border);
    color: var(--omni-text-muted);
    font-family: var(--omni-font-body);
    font-size: 0.78rem;
    font-weight: 500;
    letter-spacing: 0.02em;
    padding: 0.4rem 0.85rem;
    border-radius: var(--omni-radius-pill);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    min-width: 108px;
    transition: color 0.18s ease, background 0.18s ease, border-color 0.18s ease;
    white-space: nowrap;
}
.omni-filter-trigger:hover {
    color: var(--omni-text);
    border-color: color-mix(in srgb, var(--text) 22%, transparent);
}
.omni-filter-trigger.active {
    color: var(--omni-text);
    background: color-mix(in srgb, var(--text) 6%, transparent);
    border-color: color-mix(in srgb, var(--text) 22%, transparent);
}
.omni-filter-trigger i { font-size: 0.8rem; }

.omni-filter-trigger-count {
    font-size: 0.7rem;
    font-weight: 700;
    background: var(--omni-text);
    color: var(--omni-bg);
    padding: 0.04rem 0.45rem;
    border-radius: var(--omni-radius-pill);
    min-width: 1.25rem;
    text-align: center;
    line-height: 1.4;
}

/* Only the FOC button (the one with aria-describedby) summons the bubble.
   Reset opacity + pointer-events on both paths the shared .cc-explainer rule
   (theme.css) can open the bubble through — :hover AND :focus-within, since
   .omni-foc-explainer now wraps all three segment buttons and tabbing onto
   Week/Grid would otherwise also trip the shared unconditional
   :focus-within rule. pointer-events must be reset explicitly too: at equal
   (0,3,0) specificity our later-cascade reset only wins the properties it
   declares, so without this the shared rule's `pointer-events: auto` would
   leak through and leave an invisible-but-clickable bubble over the
   toolbar. transform is intentionally left alone — it's only ever visible
   together with opacity:1 (the re-enable rules below), where the
   mobile/tablet media-query transform rules still need to win; overriding
   transform here at this specificity would fight those. Then re-open,
   re-declaring BOTH opacity and pointer-events (for the same reason), only
   when the FOC button itself (or, for mine-mode, the .cc-explainer__trigger
   button) is the thing being hovered/focused.

   The re-open paths key on :hover and :focus-VISIBLE, never :focus-within.
   Clicking a <button> leaves it focused in Chrome and Firefox, so a
   :focus-within re-open kept the bubble at opacity:1 — and, worse,
   pointer-events:auto — hanging over the toolbar from the moment the user
   clicked FOC until they happened to click somewhere else entirely. Measured
   in Chromium against this file: hover-then-leave correctly returned to
   opacity 0, click-then-leave stayed at 1 for both the segment FOC tab and
   the My List "FOC week" trigger. :focus-visible does not match a mouse
   click, so keyboard users still get the bubble on Tab while clicking is
   silent; the [aria-describedby]:focus-visible path covers BOTH triggers,
   which is why neither :focus-within selector is needed for a11y. The
   :focus-within SUPPRESSOR above stays — it is what neutralises the shared
   .cc-explainer rule in theme.css, which does open on plain focus. */
.omni-foc-explainer:hover > .cc-explainer__bubble,
.omni-foc-explainer:focus-within > .cc-explainer__bubble {
    opacity: 0;
    pointer-events: none;
}
.omni-foc-explainer:has(.cc-tab[aria-describedby]:hover) > .cc-explainer__bubble,
.omni-foc-explainer:has([aria-describedby]:focus-visible) > .cc-explainer__bubble,
.omni-foc-explainer:hover > .cc-explainer__trigger ~ .cc-explainer__bubble {
    opacity: 1;
    pointer-events: auto;
}

@media (max-width: 640px) {
    .omni-pub-chip { min-height: 44px; }
    .omni-pub-logo { height: 20px; }

    .omni-header,
    .omni-shell { padding: 0 0.75rem; }
    /* Fit all three tabs across the width instead of scrolling them.
       overflow-x:auto is kept only as a safety net — at these sizes the 3
       tabs fit, so space-between spreads them edge-to-edge and it never
       actually scrolls. The compound selector out-specifies the shared
       `.cc-tabs[data-cc-variant="underline"]` gap (2.4rem) so the tight
       mobile gap actually applies (media queries add no specificity). */
    .omni-tabs.cc-tabs[data-cc-variant="underline"] {
        gap: 0.4rem;
        justify-content: space-between;
        flex-wrap: nowrap;
        overflow-x: auto;
        scrollbar-width: none;
    }
    .omni-tabs::-webkit-scrollbar { display: none; }
    .omni-tab {
        font-size: 0.86rem;
        padding: 0.7rem 0.1rem 0.85rem;
        gap: 0.35rem;
        flex: 0 0 auto;
        min-height: 44px;            /* tap target */
        text-align: center;
        white-space: nowrap;
    }
    .omni-tab-label { white-space: nowrap; }
    /* "Recently Released" -> "Recent". Measured on WebKit (iOS Safari's
       engine) at 390px signed in, the three full-length tabs total 411px in a
       334px row: the My List tab was cut in half by the right page edge, and
       because the scrollbar is hidden above there was no affordance showing it
       had scrolled. Shortening the middle label is what buys the row back --
       the count badges are load-bearing information and the font is already
       clamped twice below. */
    .omni-tab-label-long { display: none; }
    .omni-tab-label-short { display: inline; }
    .omni-tab-count {
        padding-left: 0.3rem;
        margin-left: 0;
        white-space: nowrap;
    }
    .omni-tab-count:not([data-omni-count-placeholder]):not(.has-count) { display: none; }
    .omni-tab-count[data-omni-count-placeholder],
    .omni-tab-count.has-count {
        display: inline-block;
        min-width: 2.1ch;
        padding-left: 0.25rem;
    }
    .omni-publisher-grouping,
    .omni-view-segment,
    .omni-shape-segment,
    .omni-ghost,
    .omni-paperbacks-switch,
    .omni-filter-trigger { flex: 0 0 auto; }
    .omni-publisher-grouping .cc-tab,
    .omni-view-segment .cc-tab,
    .omni-shape-segment .cc-tab { min-height: 44px; padding: 0.55rem 0.7rem; }
    .omni-ghost,
    .omni-paperbacks-switch,
    .omni-filter-trigger { min-height: 44px; padding: 0.55rem 0.7rem; }
    /* The trigger no longer lives in the toolbar — it sits in .omni-searchbar,
       which stays in normal flow, so `order` has nothing left to order it
       against. It stays icon-only (the sheet it opens carries the word
       "Filters" as its own title, and the search box is the row's primary
       affordance), which leaves two of the old squaring-off rules still
       earning their place and two not:
         - min-width 44px KEPT — the tap-target floor; a lone funnel glyph is
           ~13px wide and would otherwise miss it.
         - justify-content KEPT — with the label hidden the content is narrower
           than that floor, so without centring the icon sits hard left of a
           44px button.
         - width 44px DROPPED — a hard width clips the active-filter count
           badge that sits beside the funnel whenever a filter is on.
         - padding-inline 0 DROPPED — it only existed to make the fixed 44px
           square work; the shared 0.7rem inline padding above gives the badge
           room to breathe. */
    .omni-filter-trigger { min-width: 44px; justify-content: center; }
    .omni-filter-trigger-label { display: none; }

    /* GH-123. On the calendar page the searchbar holds ONLY the filter
       trigger -- the search box is not rendered there (see omnis.html) -- so
       the row was a full band spent on one right-aligned funnel between the
       tabs and the month nav. Fold it into the tab row instead: the header
       becomes a wrapping flex row where every child keeps its own line except
       the tabs (which take the remaining width) and the searchbar (which
       shrinks to the trigger). Keyed on the server-stamped page kind and
       scoped to this block: the tracker's searchbar carries the search box
       and keeps its row, and desktop keeps the shared header layout. */
    .omni-header[data-page-kind="calendar"] { display: flex; flex-wrap: wrap; align-items: center; }
    .omni-header[data-page-kind="calendar"] > * { flex: 0 0 100%; }
    .omni-header[data-page-kind="calendar"] .omni-tabs { flex: 1 1 0; min-width: 0; margin-bottom: 0; }
    .omni-header[data-page-kind="calendar"] .omni-searchbar { flex: 0 0 auto; margin: 0 0 0 0.5rem; }
    .omni-header[data-page-kind="calendar"] .omni-active-filters { margin-top: 0.75rem; }
    .omni-switch-label-long { display: none; }
    .omni-switch-label-short { display: inline; }
    .omni-paperbacks-switch .omni-switch-track { width: 38px; height: 22px; }
    .omni-paperbacks-switch .omni-switch-thumb { width: 18px; height: 18px; }
    .omni-paperbacks-switch.is-active .omni-switch-thumb { transform: translateX(16px); }

    /* ---- .omni-toolbar becomes a bottom sheet ----
       Every secondary control already lives inside it (Task 2 re-homed the
       edition types in), so nothing needs duplicating: the whole element
       simply leaves normal flow.

       flex-wrap is explicitly nowrap: the inline-row rule this replaces wrapped
       on purpose, but a COLUMN flex container with a definite max-height wraps
       into a second column instead of overflowing, which would silently strand
       the lower sections outside the scroll area. */
    .omni-toolbar {
        position: fixed;
        inset-inline: 0;
        bottom: 0;
        z-index: 1050;
        flex-direction: column;
        flex-wrap: nowrap;
        align-items: stretch;
        gap: 0.85rem;
        max-height: 82vh;
        overflow-y: auto;
        overscroll-behavior: contain;
        /* No padding-bottom. A `position: sticky; bottom: 0` child pins to the
           SCROLLPORT'S PADDING BOX, so any padding-bottom here becomes a strip
           below the pinned footer that scrolling content slides through —
           measured as a 16px bleed with chip tops visible beneath the
           Clear all / Show N row. The safe-area allowance moved onto
           .omni-sheet-foot's own padding-bottom, which is the element that
           actually has to clear the home indicator. */
        padding: 0 1rem;
        background: var(--omni-surface);
        border-top: 1px solid var(--omni-border);
        border-radius: var(--omni-radius) var(--omni-radius) 0 0;
        box-shadow: 0 -8px 32px color-mix(in srgb, var(--shadow-ink) 45%, transparent);
        transform: translateY(101%);
        visibility: hidden;
        /* Visibility must flip with the state so opening can focus a control
           at Alpine's next tick, independently of motion preferences. */
        transition: transform 0.26s var(--ease-enter, ease), visibility 0s;
    }
    /* Open state rides a class on .omni-header, bound in the template.
       The toolbar has no ancestor that reflects showFilters on its own, and a
       sibling/:has() selector would couple the sheet to the edition-type
       collapse's open state, which is the same boolean but not the same idea. */
    .omni-header.is-filters-open .omni-toolbar {
        transform: translateY(0);
        visibility: visible;
    }

    .omni-sheet-backdrop {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 1040;
        background: color-mix(in srgb, var(--shadow-ink) 55%, transparent);
        /* Stops a drag on the scrim from scrolling the page behind the sheet.
           Deliberately NOT a body overflow:hidden lock: the global
           ::-webkit-scrollbar rule in styles.css makes that cost 8px of layout
           width (scrollbar-gutter does not compensate under overflow:hidden),
           and showFilters is true on desktop too, where the sheet does not
           exist at all. The sheet's own overscroll-behavior covers the rest. */
        touch-action: none;
    }

    /* z-index 2, not 1: `.cc-tab.is-active` (cc_tabs.css) is
       `position: relative; z-index: 1`, and its `.cc-tabs` ancestor is
       `z-index: auto` so it creates no stacking context — the active segment
       tab therefore competes with this head directly in the sheet's stacking
       context. At z-index 1 they tie and DOM order decides, which the tab wins:
       once the sheet scrolls (any viewport under ~683px tall) the Week/FOC
       segment was painted straight across the pinned head, hiding the "Filters"
       title. Verified with elementsFromPoint over the title at 390x568. */
    .omni-sheet-head {
        display: grid;
        grid-template-columns: 1fr auto;
        align-items: center;
        gap: 0.5rem;
        position: sticky;
        top: 0;
        z-index: 2;
        padding: 0.85rem 0 0.7rem;
        background: var(--omni-surface);
        border-bottom: 1px solid var(--omni-border);
    }
    .omni-sheet-handle {
        grid-column: 1 / -1;
        justify-self: center;
        width: 36px;
        height: 4px;
        border-radius: var(--omni-radius-pill);
        background: color-mix(in srgb, var(--text) 22%, transparent);
        margin-bottom: 0.6rem;
    }
    .omni-sheet-title {
        font-family: var(--omni-font-display);
        font-size: 1.05rem;
        color: var(--omni-text);
    }
    .omni-sheet-close {
        background: transparent;
        border: 0;
        color: var(--omni-text-dim);
        min-width: 44px;
        min-height: 44px;
        cursor: pointer;
    }

    .omni-controls-label {
        display: block;
        font-family: var(--omni-font-body);
        font-size: 0.7rem;
        font-weight: 600;
        letter-spacing: 0.08em;
        text-transform: uppercase;
        color: var(--omni-text-dim);
        margin-bottom: -0.35rem;
    }
    /* Same z-index story as .omni-sheet-head, only worse here: this was sticky
       with NO z-index at all, so every positioned descendant scrolling past it
       out-painted the Clear all / Show N results buttons. */
    .omni-sheet-foot {
        display: flex;
        gap: 0.6rem;
        align-items: center;
        position: sticky;
        bottom: 0;
        z-index: 2;
        /* The footer owns the home-indicator clearance, not the sheet: it is
           the element pinned over it. Keeping it here also lets the sheet run
           its padding-bottom to 0 so this pins flush to the sheet's bottom
           edge with no strip of scrolling content showing underneath. */
        padding: 0.75rem 0 max(0.75rem, env(safe-area-inset-bottom));
        background: var(--omni-surface);
        border-top: 1px solid var(--omni-border);
    }
    .omni-sheet-clear {
        flex: 0 0 auto;
        background: transparent;
        border: 1px solid var(--omni-glass-border);
        border-radius: var(--omni-radius-pill);
        color: var(--omni-text-muted);
        font-family: var(--omni-font-body);
        font-size: 0.85rem;
        min-height: 44px;
        padding-inline: 1rem;
        cursor: pointer;
    }
    .omni-sheet-apply {
        flex: 1 1 auto;
        background: var(--omni-text);
        border: 0;
        border-radius: var(--omni-radius-pill);
        color: var(--omni-bg);
        font-family: var(--omni-font-body);
        font-size: 0.9rem;
        font-weight: 700;
        min-height: 44px;
        cursor: pointer;
    }

    /* Inside the sheet the edition types are a peer section, not a nested
       disclosure: the collapse wrapper is gated on the same showFilters boolean
       that opened the sheet, so its animation would only ever duplicate the
       sheet's own. Neutralise the whole collapsed presentation (rows, opacity
       and offset), not just the row track. */
    .omni-type-collapse {
        grid-template-rows: 1fr;
        max-height: none;
        overflow: visible;
        opacity: 1;
        transform: none;
    }
    .omni-type-collapse > .cc-motion-collapse-inner { overflow: visible; }
    /* Left-aligned like every other sheet section; the sheet's own gap supplies
       the spacing the standalone bar used to need.

       flex-wrap and overflow-x are UNDOING the second mobile block further down
       this file (`.omni-filter-bar { overflow-x: auto; flex-wrap: nowrap }`),
       which turned the bar into a horizontal swipe row back when it was a
       standalone drawer below the header. Inside the sheet that rule made
       filters unreachable rather than swipeable: `.omni-chip` is
       `flex-shrink: 0`, so with nowrap the bar's MIN-CONTENT width is the sum
       of every chip. `.omni-type-collapse` is a flex item with the default
       `min-width: auto`, so it refuses to shrink below that and the whole
       subtree inflates past the sheet — measured 952px inside a 358px content
       box, with 6 of 9 format chips off the right edge. The bar's own
       `overflow-x: auto` could not save it because clientWidth == scrollWidth:
       nothing was constraining the bar, so there was nothing to scroll. This
       rule is (0,2,0) and beats the (0,1,0) one regardless of source order. */
    .omni-type-collapse .omni-filter-bar {
        flex-wrap: wrap;
        overflow-x: visible;
        justify-content: flex-start;
        margin-bottom: 0;
        padding-bottom: 0;
    }

    /* Publisher chips are a labelled section now, not a wrapping toolbar row.
       No mobile override is needed to say so: the base `.omni-publisher-group`
       rule already resolves to `flex-basis: auto` and declares neither
       `justify-content` nor `padding-top`, so the old override restated
       defaults it was never fighting. */

    /* Content width, not full width. The sheet is a column flex container, so
       its default `align-items: stretch` spans every direct child edge to edge
       — which left the two-icon shape segment floating in a 358px-wide box and
       made the paperbacks toggle read as a banner. The Group-by segment escaped
       only by accident: it is wrapped in the inline-flex
       `<span class="cc-explainer omni-foc-explainer">`, so it sizes to content.
       align-self on the two offenders rather than align-items on the sheet: the
       head, footer, chip rows and publisher group legitimately want the full
       width. */
    .omni-shape-segment,
    .omni-paperbacks-switch { align-self: flex-start; }

    /* The active-filter row is the only place filters show at rest. */
    .omni-active-filters {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 0.4rem;
        margin-bottom: 0.75rem;
    }

    /* The FOC bubble anchored to `.omni-toolbar { position: relative }` while
       the toolbar was in normal flow. The toolbar is now position:fixed, so
       that containing block moved — anchor the bubble to its own explainer
       instead, which is stable in both presentations. */
    .omni-foc-explainer { position: relative; }
    /* The bubble also has to FLIP: the shared primitive opens upward
       (`bottom: calc(100% + .55rem)`), which was right when the explainer sat
       in a toolbar with the whole page above it. Inside the sheet there is
       nothing above it but the sticky .omni-sheet-head, and the sheet is a
       scroll container (overflow-y: auto), so upward is the one direction that
       does not work here:
         - unscrolled it lands 27px into the sticky head, over the "Filters"
           title;
         - once the sheet scrolls (any viewport under ~683px tall, where
           max-height:82vh drops below the sheet's content height) it is clipped
           by the scrollport's top edge — measured 52% cut off at 390x568, with
           only the last word readable. Block-start overflow in a scroll
           container is unreachable, so no amount of scrolling recovers it.
       Opening downward puts it over the sheet body, which is both scrollable
       and empty enough to host it. `bottom: auto` is required: `top` alone
       would leave both insets set and the shared `bottom` would still win the
       block axis. */
    .omni-foc-explainer .cc-explainer__bubble {
        top: calc(100% + 0.55rem);
        bottom: auto;
        left: 0;
        right: auto;
        margin-inline: 0;
        max-width: min(18rem, calc(100vw - 3rem));
        transform: translateY(0.2rem) scale(0.98);
    }
    .omni-foc-explainer:hover > .cc-explainer__bubble,
    .omni-foc-explainer:focus-within > .cc-explainer__bubble {
        transform: translateY(0) scale(1);
    }
}

@media (min-width: 640.1px) and (max-width: 1024px) {
    .omni-foc-explainer .cc-explainer__bubble {
        left: 0;
        transform: translateY(0.2rem) scale(0.98);
    }

    .omni-foc-explainer:hover > .cc-explainer__bubble,
    .omni-foc-explainer:focus-within > .cc-explainer__bubble {
        transform: translateY(0) scale(1);
    }
}

/* Extra clamp for the smallest phones so "Recently Released" still fits. */
@media (max-width: 380px) {
    .omni-header,
    .omni-shell { padding: 0 0.25rem; }
    .omni-tabs.cc-tabs[data-cc-variant="underline"] { gap: 0.3rem; }
    .omni-tab { font-size: 0.76rem; gap: 0.15rem; }
    .omni-tab-mine .omni-tab-heart { width: 11px; height: 11px; }
    .omni-tab-count { font-size: 0.62rem; }
    /* The 2.1ch reservation above exists to stop a 1->2 digit count from
       nudging the row; at 320px that slack is the last 3px between fitting
       and clipping the My List tab, and the counts here are already 2-4
       digits, so the reservation buys nothing it costs. */
    .omni-tab-count[data-omni-count-placeholder],
    .omni-tab-count.has-count { min-width: 0; padding-left: 0.2rem; }
}

/* ============================================================
   6. Publisher Filter Chips
   ============================================================ */
.omni-filter-bar {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    justify-content: center;
    margin-bottom: 1.5rem;
}

/* .omni-chip is a thin extension of the shared .cc-chip (radius, cursor,
   white-space, transition, and font-family/size already come from .cc-chip)
   — this rule now carries only omnis-specific deltas: the translucent
   frosted-glass border/background (vs. .cc-chip's opaque
   var(--border)/var(--surface)) and a lighter font-weight. The only
   `.active` color mapping left is `.omni-filter-bar.edition-type
   .omni-chip.active` below (drawer chips use class `active`, not the
   shared `.is-active`) — the per-mode mappings were dropped when the
   publisher drawer chips (their only `mode-` ancestor) were removed.
   Border/background/font-weight here override .cc-chip's by cascade
   order (omnis.css loads after the shared site bundle), which is what
   keeps the glass look working with backdrop-filter. */
.omni-chip {
    padding: 0.4rem 0.9rem;
    border: 1px solid var(--omni-glass-border);
    background: var(--omni-glass-bg);
    backdrop-filter: blur(8px);
    color: var(--omni-text-muted);
    font-weight: 500;
}

.omni-chip:hover {
    border-color: color-mix(in srgb, var(--text) 25%, transparent);
    color: var(--omni-text);
}

/* The manga toggle sits in its own bar (the Format bar hides itself when the
   edition-type facet is thin, and this control must not vanish with it), so it
   is named here to inherit the same active treatment rather than defining a
   second one. */
.omni-filter-bar.edition-type .omni-chip.active,
.omni-filter-bar.omni-manga-bar .omni-chip.active {
    background: var(--omni-accent-edition-type);
    border-color: var(--omni-accent-edition-type);
    color: var(--shadow-ink);
    font-weight: 600;
}

/* Section 7 ("Active Filter Badge") is gone: its single `.omni-active-filter`
   rule was orphaned — no template ever carried the class, only the unrelated
   plural `.omni-active-filters` row. It is deleted rather than left in place
   because the live chip rule now owns that exact name; a second (0,1,0) rule
   this far down the file would win on source order and silently replace the
   chip's background, border and asymmetric padding. */

/* ============================================================
   8. Card Grid
   ============================================================ */


/* Skip layout/paint/style for off-screen week/FOC groups — the default view is
   grouped-by-week, so deep scroll walks many groups. Mirrors shelf.css's
   .shelf-wall-collapse-inner: content-visibility lives on the group's inner
   collapse wrapper, which ALREADY clips via `overflow: hidden`, so contain:paint
   adds no new hover-shadow clipping (unlike per-card placement), and it sits
   below the animated .cc-motion-collapse node so the collapse animation and
   content-visibility never contend.
   The reserve is per-GROUP and week groups vary widely (1 comic to 15+), so no
   fixed value fits all. `auto` makes this a one-time-per-group cost: the real
   height is remembered after first paint, and first-encounter growth happens
   below the fold (scroll-anchoring absorbs it). We bias LOW — ~one card row —
   so busy weeks grow (harmless, below-fold) rather than over-reserving and
   shrinking. */
.omni-grid-collapse > .cc-motion-collapse-inner {
    content-visibility: auto;
    contain-intrinsic-size: auto 450px;
}

/* Faster, asymmetric timing for the week/FOC group fold — an override of the
   shared Recipe D default, scoped to THIS wrapper only.

   `grid-template-rows` is a LAYOUT property, so every frame of the transition
   re-runs layout, and because the group's height change reflows everything
   below it the cost scales with how many cards are on the PAGE, not how big
   the folded group is. It therefore gets worse the further the reader has
   paginated — which is exactly when they are folding weeks away.

   Measured against these sheets at 20 groups x 15 cards (300 cards), layout
   passes and layout time per fold, from CDP Performance metrics:
     --motion-slow 360ms (Recipe D default) : 44 passes,  7.4ms  (all 20: 55.4ms)
     --motion-fast 140ms (here)             : ~18 passes, ~2.5ms (all 20:  ~7ms)
   Halving the duration halves the passes, and the perceived wait drops with it.

   Asymmetric because closing and opening want different things: a fold the
   reader asked for should get out of the way (--motion-fast/--ease-exit,
   matching the modal's close direction — see theme.css), while an unfold is a
   reveal and keeps a little more travel (--motion-med). The transition used is
   the one on the DESTINATION state, so the base rule times the close and the
   .is-open rule times the open.

   Scoped rather than retimed at .cc-motion-collapse: that class is shared by
   the shelf, the reading-order editor, admin and marketplace, whose panels are
   small and don't pay this cost. Equal specificity to the theme.css rule, so
   this depends on omnis.css loading AFTER the site bundle — base.html emits
   asset_tags('site') above {% block extra_css %}, which is where this sheet is
   linked. The prefers-reduced-motion `transition: none !important` in theme.css
   still wins over both, so the no-motion path is untouched.

   NOT done here: `contain: layout` on .omni-foc-section, which measured better
   still (55.4ms -> 15.3ms when folding every group). It gives each section its
   own stacking context, and .omni-foc-header is `position: sticky; z-index: 5`
   INSIDE that section — the pinned week header would stop painting over the
   next group's cards. */
.omni-grid-collapse {
    transition:
        grid-template-rows var(--motion-fast) var(--ease-exit),
        opacity var(--motion-fast) var(--ease-exit),
        transform var(--motion-fast) var(--ease-exit);
}

.omni-grid-collapse.is-open {
    transition:
        grid-template-rows var(--motion-med) var(--ease-standard),
        opacity var(--motion-med) var(--ease-standard),
        transform var(--motion-med) var(--ease-standard);
}

/* Un-clip paint for the group that currently has a hovered tile so the tile's
   hover lift (`translateY(-8px) scale(1.02)`) and its glow shadow can bleed past
   the group box. The group wrapper is `content-visibility: auto` (implies
   `contain: paint`) and the shared `.cc-motion-collapse-inner { overflow: hidden }`
   otherwise crops that glow on edge tiles. Scope it to `:has(...:hover)` so
   resting and off-screen groups keep their render-skipping + clipping.

   Deliberately NOT here: dropping the group's `transform: translateY(0)`. That
   used to dissolve the group's stacking context so a hovered first-row tile
   could rise *over* the pinned sticky week header — but toggling that transform
   on/off with every hover tore down and rebuilt the group's GPU compositing
   layer, which holds the cards' `backdrop-filter` badges, re-rasterizing it from
   an empty buffer. During scroll (where the hovered tile changes continuously)
   that was a frame-rate flash-to-black. The transform now stays stable, so the
   hovered tile tucks UNDER the pinned header (its z-index:5 sits outside the
   group's stacking context) — the over-header lift is intentionally gone. */
.omni-grid-collapse:has(.omni-card-shell:hover) > .cc-motion-collapse-inner {
    content-visibility: visible;
    overflow: visible;
}
/* `.omni-card-shell` is already `position: relative` (see section 9); this lifts
   the hovered card above its in-group siblings so its glow overlaps them cleanly.
   It stays within the group's stacking context — it does not clear the header. */
.omni-card-shell:hover {
    z-index: 6;
}

/* ============================================================
   9. Cards
   ============================================================ */






/* The teal owned-ring lived here. Retired in favour of the shared ownership
   spine (ownership_spine.css), for two reasons:

   1. It failed the 3:1 non-text contrast floor. --accent-recent measured
      2.93:1 against --surface-2 in the default light theme and 2.65:1-2.87:1
      in the dc, midnight-vault and newsstand light palettes.
   2. It said only "owned", while /search said the same thing with a green
      `badge bg-success` pill and neither could say "you own this in another
      format". Three card systems, three dialects.

   `.is-owned` is still applied by omniCardClass() and is left un-styled on
   purpose: it is a useful hook (and is asserted by the omnis tests), it just
   no longer draws a ring. */

/* ============================================================
   10. Cover Image
   ============================================================ */



/* ============================================================
   11. Countdown / Recency Badges
   ============================================================ */

/* NO pulse-glow keyframes here — deliberately. The .today date badge used to
   pulse forever, but the badge carries `backdrop-filter` (comic_tile.css), and
   animating a backdrop sampler makes Firefox re-blur its backdrop every frame:
   on release days that pegged the GPU process while idle and starved scroll
   rasterization into full-viewport black flashes. The badge keeps a static
   glow in comic_tile.css. Pinned by
   test_omnis_no_animation_on_backdrop_filter.py. */


/* ============================================================
   12. Card Body
   ============================================================ */






/* ============================================================
   13. Summary & Empty State
   ============================================================ */

/* Pointer to a collected line's own series page (see omnis.html). The look is
   the search page's `.search-line-nudge`, in styles.css — same banner, same
   job, so it is deliberately shared rather than restyled. Only the width
   differs: that page lays its results out in a 1200px column and the shared
   rule centres the banner inside it, while here the banner sits inside the
   same `.omni-shell` as the grid, which already caps and centres the column
   (`--omni-page-max`) and carries the responsive gutters. So the shared
   1200px cap and auto margins come off, and the banner fills the shell and
   sits flush over the first card at every width. */
.omni-line-nudge {
    max-width: none;
    margin-left: 0;
    margin-right: 0;
}

/* The default-scope exclusion note (GH-1089) wears the same banner, as a
   button: the browser's button defaults (centred text, system font, no full
   width) come off so it lays out exactly like the anchor version. */
.omni-scope-note {
    width: 100%;
    text-align: left;
    font-family: inherit;
    cursor: pointer;
}

.omni-scope-note-action {
    flex: none;
    color: var(--brand);
    font-weight: 600;
}

.omni-summary {
    text-align: center;
    margin-bottom: 1.25rem;
    min-height: 24px;
    font-size: 0.85rem;
    color: var(--omni-text-muted);
}

.omni-empty {
    text-align: center;
    padding: 4rem 1rem;
    color: var(--omni-text-dim);
}

.omni-empty i {
    font-size: 3rem;
    margin-bottom: 1rem;
    display: block;
    opacity: 0.3;
}

/* No-match query: the same text searched across the whole catalog */
.omni-catalog-fallback {
    max-width: 960px;
    margin: 2rem auto 0;
}

.omni-catalog-fallback-heading {
    font-size: 1rem;
    font-weight: 600;
    color: var(--omni-text);
    margin-bottom: 1rem;
}

.omni-catalog-fallback-list {
    list-style: none;
    margin: 0 0 1.25rem;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr));
    gap: 0.75rem;
    text-align: left;
}

.omni-catalog-fallback-item {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    height: 100%;
    padding: 0.5rem;
    background: var(--omni-surface);
    border: 1px solid var(--omni-border);
    border-radius: 8px;
    color: var(--omni-text);
    text-decoration: none;
}

.omni-catalog-fallback-item:hover,
.omni-catalog-fallback-item:focus-visible {
    border-color: var(--border-strong);
}

.omni-catalog-fallback-cover {
    flex: none;
    width: 48px;
    height: 72px;
    object-fit: cover;
    border-radius: 4px;
}

.omni-catalog-fallback-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.omni-catalog-fallback-title {
    font-size: 0.9rem;
    line-height: 1.3;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.omni-catalog-fallback-meta {
    font-size: 0.8rem;
    color: var(--omni-text-muted);
}

/* "My List" empty state — warm scarlet atmosphere instead of generic muted */
.omni-empty-mine {
    padding: 5rem 1.25rem 4rem;
    position: relative;
    isolation: isolate;
}
.omni-empty-mine::before {
    /* soft scarlet halo behind the heart */
    content: '';
    position: absolute;
    inset: 0;
    background: radial-gradient(
        ellipse 360px 220px at 50% 38%,
        color-mix(in srgb, var(--omni-marvel-red) 18%, transparent),
        transparent 70%
    );
    pointer-events: none;
    z-index: -1;
}
.omni-empty-mine-heart {
    width: 64px;
    height: 64px;
    stroke: var(--omni-accent-mine-soft);
    fill: none;
    stroke-width: 1.5;
    stroke-linecap: round;
    stroke-linejoin: round;
    margin: 0 auto 1.2rem;
    display: block;
    opacity: 0.85;
    filter: drop-shadow(0 0 12px color-mix(in srgb, var(--omni-marvel-red) 35%, transparent));
    animation: empty-heart-pulse 3.6s ease-in-out infinite;
}
@keyframes empty-heart-pulse {
    0%, 100% { transform: scale(1); opacity: 0.85; }
    50%      { transform: scale(1.06); opacity: 1; }
}
.omni-empty-mine-headline {
    font-family: var(--omni-font-display);
    font-size: 1.5rem;
    letter-spacing: 0.01em;
    color: var(--omni-text);
    margin: 0 0 0.6rem;
}
.omni-empty-mine-sub {
    font-family: var(--omni-font-body);
    font-size: 0.95rem;
    color: var(--omni-text-muted);
    max-width: 32ch;
    margin: 0 auto 1.5rem;
    line-height: 1.5;
}
.omni-empty-mine-heart-inline {
    color: var(--omni-accent-mine-soft);
    font-size: 1.05em;
    line-height: 1;
}

@keyframes shimmer {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

/* ============================================================
   15. Error State
   ============================================================ */
.omni-error {
    background: color-mix(in srgb, var(--accent-interest) 10%, transparent);
    border: 1px solid color-mix(in srgb, var(--accent-interest) 30%, transparent);
    border-radius: var(--omni-radius);
    padding: 1rem 1.5rem;
    color: var(--omni-error-fg);
    text-align: center;
    margin-bottom: 1.5rem;
}

/* ============================================================
   16. Dark Modal
   ============================================================ */
.omni-modal {
    background: var(--omni-surface);
    border: 1px solid var(--omni-border);
    color: var(--omni-text);
    transition: height 0.35s ease, max-height 0.35s ease;
}

.omni-modal .modal-header {
    border-bottom-color: var(--omni-border);
    justify-content: flex-start;
}

.omni-modal .modal-title {
    min-width: 0;
    padding-right: 1rem;
}

.omni-modal .modal-body { transition: height 0.35s ease; }

.omni-modal .modal-title {
    font-family: var(--omni-font-body);
    font-weight: 700;
}

.omni-modal .table {
    color: var(--omni-text);
    --bs-table-bg: transparent;
}

.omni-modal .table th {
    color: var(--omni-text-muted);
    border-color: var(--omni-border);
    font-weight: 500;
}

.omni-modal .table td {
    border-color: var(--omni-border);
}

.omni-modal h6 {
    color: var(--omni-text);
    font-weight: 600;
}

.omni-modal .text-muted {
    color: var(--omni-text-muted) !important;
}

.omni-issue-badge {
    background: var(--omni-glass-bg);
    border: 1px solid var(--omni-glass-border);
    color: var(--omni-text);
    font-weight: 400;
}

.omni-detail-cover {
    max-height: 400px;
    object-fit: contain;
    border-radius: var(--omni-radius-sm);
}

.omni-share {
    margin-top: 0.75rem;
}

.omni-share-btn {
    width: 100%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.45rem;
}

.omni-share-feedback {
    margin-top: 0.35rem;
    font-size: 0.78rem;
    color: var(--success);
}

.omni-share-feedback.is-error {
    color: var(--danger);
}

/* ============================================================
   17. Custom Scrollbar
   ============================================================ */
[data-theme="dark"] ::-webkit-scrollbar {
    width: 8px;
}

[data-theme="dark"] ::-webkit-scrollbar-track {
    background: var(--omni-bg);
}

[data-theme="dark"] ::-webkit-scrollbar-thumb {
    background: var(--omni-border);
    border-radius: 4px;
}

[data-theme="dark"] ::-webkit-scrollbar-thumb:hover {
    background: var(--border-strong);
}

/* ============================================================
   18. Responsive
   ============================================================ */

/* Tablet */
@media (max-width: 767.98px) {
    .omni-modal .modal-body .row {
        flex-direction: column;
    }

    .omni-modal .modal-body .col-md-4 {
        margin-bottom: 1rem;
    }

    .omni-modal .modal-body .col-md-4 img {
        max-height: 280px;
    }
}

/* Mobile */
@media (max-width: 640px) {
    .container-fluid {
        padding-left: 1rem;
        padding-right: 1rem;
    }

    .omni-filter-bar {
        overflow-x: auto;
        flex-wrap: nowrap;
        justify-content: flex-start;
        padding-bottom: 0.5rem;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
    }

    .omni-filter-bar::-webkit-scrollbar {
        display: none;
    }

    .omni-chip {
        flex-shrink: 0;
        min-height: 40px;
        display: flex;
        align-items: center;
    }
}

/* ============================================================
   Source Badges (card grid)
   ============================================================ */
.omni-source-badges {
    display: flex;
    gap: 4px;
    flex-wrap: wrap;
    margin-top: 6px;
}

.omni-source-badge {
    font-size: 0.6rem;
    font-weight: 600;
    letter-spacing: 0.03em;
    padding: 1px 6px;
    border-radius: var(--omni-radius-sm);
    background: color-mix(in srgb, var(--text) 8%, transparent);
    color: var(--omni-text-muted);
    line-height: 1.5;
}

.omni-source-badge.source-locg { color: var(--omni-source-locg); background: color-mix(in srgb, var(--omni-source-locg) 12%, transparent); }
.omni-source-badge.source-prh  { color: var(--omni-source-prh); background: color-mix(in srgb, var(--omni-source-prh) 12%, transparent); }
.omni-source-badge.source-lunar { color: var(--omni-source-lunar); background: color-mix(in srgb, var(--omni-source-lunar) 12%, transparent); }
.omni-source-badge.source-edelweiss { color: var(--omni-source-edelweiss); background: color-mix(in srgb, var(--omni-source-edelweiss) 12%, transparent); }

/* ============================================================
   Source Pills (detail modal)
   ============================================================ */
.omni-source-pill {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 0.8rem;
    padding: 4px 12px;
    border-radius: var(--omni-radius-pill);
    background: color-mix(in srgb, var(--text) 6%, transparent);
    color: var(--omni-text);
    border: 1px solid color-mix(in srgb, var(--text) 8%, transparent);
}

.omni-source-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--omni-text-muted);
}

.omni-source-dot.source-locg { background: var(--omni-source-locg); }
.omni-source-dot.source-prh  { background: var(--omni-source-prh); }
.omni-source-dot.source-lunar { background: var(--omni-source-lunar); }
.omni-source-dot.source-edelweiss { background: var(--omni-source-edelweiss); }

/* ============================================================
   Modal Navigation (swipe + arrows)
   ============================================================ */
.omni-modal {
    position: relative;
}

#editionModal .cc-modal__panel.omni-modal {
    display: flex;
    flex-direction: column;
    max-height: 86vh;
    overflow: visible;
}

#editionModal .modal-body {
    min-height: 0;
    max-height: calc(86vh - 5.75rem);
    overflow-y: auto;
}

.omni-nav-arrow {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 10;
    background: color-mix(in srgb, var(--shadow-ink) 50%, transparent);
    border: 1px solid color-mix(in srgb, var(--on-image) 15%, transparent);
    color: var(--omni-text);
    width: 40px;
    height: 40px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: background 0.2s, opacity 0.2s;
    opacity: 0.7;
    font-size: 1.1rem;
}

.omni-nav-arrow:hover {
    background: color-mix(in srgb, var(--on-image) 15%, transparent);
    opacity: 1;
}

.omni-nav-prev {
    left: -20px;
}

.omni-nav-next {
    right: -20px;
}

.omni-nav-pos {
    color: var(--omni-text-muted);
    font-size: 0.875rem;
    margin-right: 12px;
    white-space: nowrap;
}

.omni-modal .modal-footer {
    border-top-color: var(--omni-border);
    justify-content: flex-end;
}

/* On mobile, put arrows inside the modal */
@media (max-width: 767.98px) {
    .omni-nav-prev {
        left: 4px;
    }

    .omni-nav-next {
        right: 4px;
    }

    .omni-nav-arrow {
        width: 36px;
        height: 36px;
        opacity: 0.5;
        background: color-mix(in srgb, var(--shadow-ink) 60%, transparent);
    }
}

@media (max-width: 575.98px) {
    dialog.cc-modal.cc-modal--fullscreen-sm#editionModal {
        width: 100%;
        margin: 0;
    }

    #editionModal .cc-modal__panel.omni-modal {
        max-height: 100dvh;
    }

    #editionModal .modal-body {
        max-height: calc(100dvh - 5.75rem);
    }
}

/* ============================================================
   FOC Grouping
   ============================================================ */
.omni-foc-section {
    margin-bottom: 2rem;
}

.omni-foc-header {
    --lg-safe-bg: color-mix(in srgb, var(--omni-bg) 90%, transparent);
    --lg-safe-blur: blur(10px);
    display: flex;
    align-items: baseline;
    gap: 0.75rem;
    padding: 0.75rem 0;
    margin-bottom: 1rem;
    /* This is a real <button> (it was a div + role="button", which is not
       focusable, so the fold was mouse-only). Reset the UA button box so the
       sticky flex row above is unchanged — `border: 0` must precede the
       border-bottom below or the UA border wins the shorthand. The focus ring
       comes from theme.css's global :focus-visible; don't restate it here. */
    appearance: none;
    width: 100%;
    font: inherit;
    color: inherit;
    text-align: left;
    border: 0;
    border-bottom: 1px solid var(--omni-border);
    position: sticky;
    /* offset below the pinned .site-navbar so the group header doesn't tuck under it */
    top: var(--nav-h);
    /* iOS 26-safe: the sampled sticky element stays transparent; the shared
       .omni-foc-header::before rule in styles.css paints the frosted fill. */
    background: transparent;
    isolation: isolate;
    z-index: 5;
    cursor: pointer;
    user-select: none;
}

.omni-foc-header:hover {
    border-bottom-color: var(--omni-accent-upcoming);
}

.omni-foc-chevron {
    font-size: 0.85rem;
    color: var(--omni-text-muted);
    transition: transform 0.15s ease;
}

.omni-foc-header-label {
    font-family: var(--omni-font-display);
    font-size: 1.1rem;
    color: var(--omni-accent-upcoming);
    letter-spacing: 0.02em;
}

/* My List week-group headers wear the scarlet accent instead of FOC amber */
.mode-mine .omni-foc-header-label { color: var(--omni-accent-mine); }
.mode-mine .omni-foc-header:hover { border-bottom-color: var(--omni-accent-mine); }

.omni-foc-header-count {
    font-size: 0.8rem;
    color: var(--omni-text-muted);
}

/* The 51px grouping row is useful context in portrait, but keeping it pinned
   above a landscape phone's cards pushes total chrome past a quarter of the
   viewport. It remains the first row of its group and scrolls normally here. */
@media (max-height: 480px) {
    .omni-foc-header {
        position: static;
    }
}

/* Demarcation between current/future FOC groups and past ones (Upcoming view) */
.omni-foc-divider {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    margin: 2.5rem 0 1.25rem;
    color: var(--omni-text-muted);
    font-family: var(--omni-font-display);
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.12em;
}

.omni-foc-divider::before,
.omni-foc-divider::after {
    content: "";
    flex: 1 1 auto;
    height: 1px;
    background: var(--omni-border);
}

/* ============================================================
   12. Calendar View
   ============================================================ */

.omni-cal-header {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 1rem;
    margin-bottom: 1.5rem;
    padding-top: 0.5rem;
}

.omni-cal-month-label {
    font-family: var(--omni-font-display);
    font-size: 1.5rem;
    color: var(--omni-text);
    min-width: 240px;
    text-align: center;
    margin: 0;
}

.omni-cal-nav {
    background: var(--omni-surface);
    border: 1px solid var(--omni-border);
    color: var(--omni-text);
    width: 36px;
    height: 36px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: background 0.2s;
}

.omni-cal-nav:hover {
    background: var(--omni-surface-hover);
    border-color: var(--omni-text-muted);
}

.omni-cal-today-btn {
    padding: 0.35rem 0.9rem;
    border-radius: var(--omni-radius-pill);
    border: 1px solid var(--omni-glass-border);
    background: var(--omni-glass-bg);
    color: var(--omni-text-muted);
    font-family: var(--omni-font-body);
    font-size: 0.8rem;
    cursor: pointer;
    transition: background 0.2s, color 0.2s;
}

.omni-cal-today-btn:hover {
    background: var(--omni-surface-hover);
    color: var(--omni-text);
}

/* Subscribe row (GH-1012): wraps rather than overflowing a 390px viewport. */
.omni-cal-subscribe {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 0.5rem 0.75rem;
    margin: -0.5rem 0 1.25rem;
    font-size: 0.8rem;
    color: var(--omni-text-muted);
}

.omni-cal-subscribe-label {
    color: var(--omni-text);
}

.omni-cal-subscribe-link {
    display: inline-flex;
    align-items: center;
    padding: 0.35rem 0.9rem;
    border-radius: var(--omni-radius-pill);
    border: 1px solid var(--omni-glass-border);
    background: var(--omni-glass-bg);
    color: var(--omni-text);
    text-decoration: none;
}

.omni-cal-subscribe-link:hover,
.omni-cal-subscribe-link:focus-visible {
    background: var(--omni-surface-hover);
    color: var(--omni-text);
}

.omni-cal-subscribe-help {
    flex-basis: 100%;
    text-align: center;
    min-width: 0;
}

.omni-cal-loading {
    text-align: center;
    padding: 3rem;
    color: var(--omni-text-muted);
}

/* 7-column grid */
.omni-cal-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    max-width: 1200px;
    margin: 0 auto;
}

.omni-cal-weekdays {
    border: 1px solid var(--omni-border);
    border-bottom: none;
    border-radius: var(--omni-radius) var(--omni-radius) 0 0;
    overflow: hidden;
    gap: 1px;
    background: var(--omni-border);
}

.omni-cal-weekday {
    background: var(--omni-surface);
    padding: 0.5rem;
    text-align: center;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    color: var(--omni-text-muted);
    letter-spacing: 0.05em;
}

.omni-cal-days {
    border: 1px solid var(--omni-border);
    border-radius: 0 0 var(--omni-radius) var(--omni-radius);
    overflow: hidden;
    gap: 1px;
    background: var(--omni-border);
}

.omni-cal-day {
    background: var(--omni-bg);
    min-height: 120px;
    padding: 0.4rem;
    display: flex;
    flex-direction: column;
}

.omni-cal-day.is-today {
    background: color-mix(in srgb, var(--accent-upcoming) 6%, transparent);
}

.omni-cal-day.is-other-month {
    opacity: 0.3;
}

.omni-cal-day.is-past .omni-cal-comic,
.omni-cal-day.is-past .omni-cal-overflow {
    opacity: 0.55;
}

.omni-cal-day.is-past .omni-cal-day-num {
    opacity: 0.65;
}

.omni-cal-agenda-day.is-past {
    opacity: 0.6;
}

.omni-cal-comic,
.omni-cal-agenda-comic,
.omni-day-list-item {
    color: inherit;
    text-decoration: none;
}

/* Day-list modal (click "+N more") */
.omni-day-list-body {
    padding: 0.5rem;
}

.omni-day-list-item {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    width: 100%;
    padding: 0.6rem 0.75rem;
    background: transparent;
    border: 1px solid transparent;
    border-radius: 8px;
    color: inherit;
    text-align: left;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
}

.omni-day-list-item + .omni-day-list-item {
    margin-top: 2px;
}

.omni-day-list-item:hover,
.omni-day-list-item:focus-visible {
    background: color-mix(in srgb, var(--text) 4%, transparent);
    border-color: color-mix(in srgb, var(--text) 8%, transparent);
    outline: none;
}

.omni-day-list-thumb {
    width: 44px;
    height: 66px;
    object-fit: cover;
    border-radius: 4px;
    flex-shrink: 0;
    background: var(--surface-2);
}

.omni-day-list-info {
    flex: 1;
    min-width: 0;
}

.omni-day-list-title {
    font-size: 0.9rem;
    font-weight: 500;
    line-height: 1.25;
    margin-bottom: 0.2rem;
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

.omni-day-list-meta {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
    font-size: 0.75rem;
    color: var(--omni-text-muted);
}

.omni-day-list-type {
    color: var(--omni-text);
    opacity: 0.75;
}

.omni-day-list-chev {
    color: var(--omni-text-muted);
    font-size: 0.9rem;
    flex-shrink: 0;
}

.omni-cal-day-num {
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--omni-text-muted);
    margin-bottom: 0.25rem;
    width: fit-content;
}

.omni-cal-day-num.is-today {
    background: var(--omni-accent-upcoming);
    color: var(--shadow-ink);
    border-radius: 50%;
    width: 22px;
    height: 22px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 0.7rem;
}

/* Comic mini-cards in day cells */
.omni-cal-day-comics {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 3px;
    overflow: hidden;
}

.omni-cal-comic {
    display: flex;
    gap: 4px;
    align-items: center;
    cursor: pointer;
    padding: 2px;
    border-radius: 4px;
    transition: background 0.15s;
}

.omni-cal-comic:hover {
    background: var(--omni-surface-hover);
}

.omni-cal-comic-thumb {
    width: 28px;
    height: 42px;
    object-fit: cover;
    border-radius: 2px;
    flex-shrink: 0;
}

.omni-cal-comic-title {
    font-size: 0.65rem;
    line-height: 1.2;
    color: var(--omni-text);
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

.omni-cal-overflow {
    font-size: 0.65rem;
    color: var(--omni-accent-upcoming);
    cursor: pointer;
    padding: 2px;
    font-weight: 600;
}

.omni-cal-overflow:hover {
    text-decoration: underline;
}

/* Agenda view: hidden on desktop, shown on mobile */
.omni-cal-agenda {
    display: none;
    max-width: 700px;
    margin: 0 auto;
}

/* ============================================================
   13. Calendar – Responsive
   ============================================================ */

/* Tablet */
@media (max-width: 767.98px) {
    .omni-cal-day {
        min-height: 90px;
    }

    .omni-cal-comic-thumb {
        width: 22px;
        height: 33px;
    }

    .omni-cal-comic-title {
        font-size: 0.6rem;
        -webkit-line-clamp: 1;
    }

    .omni-cal-month-label {
        font-size: 1.2rem;
        min-width: 200px;
    }
}

/* Mobile: switch from grid to agenda */
@media (max-width: 640px) {
    .omni-cal-desktop {
        display: none;
    }

    /* Calendar nav controls: 44px tap targets. */
    .omni-cal-nav { width: 44px; height: 44px; }
    .omni-cal-today-btn { min-height: 44px; }
    .omni-cal-subscribe-link { min-height: 44px; }

    .omni-cal-agenda {
        display: block;
    }

    .omni-cal-agenda-day {
        padding: 0.75rem 0;
        border-bottom: 1px solid var(--omni-border);
    }

    .omni-cal-agenda-date {
        display: flex;
        align-items: baseline;
        gap: 0.5rem;
        margin-bottom: 0.5rem;
    }

    .omni-cal-agenda-dayname {
        font-size: 0.7rem;
        text-transform: uppercase;
        color: var(--omni-text-muted);
        font-weight: 600;
        min-width: 2rem;
    }

    .omni-cal-agenda-daynum {
        font-size: 1.1rem;
        font-weight: 700;
        color: var(--omni-text);
    }

    .omni-cal-agenda-daynum.is-today {
        background: var(--omni-accent-upcoming);
        color: var(--shadow-ink);
        border-radius: 50%;
        width: 28px;
        height: 28px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }

    .omni-cal-agenda-comics {
        display: flex;
        flex-direction: column;
        gap: 0.25rem;
    }

    .omni-cal-agenda-comic {
        display: flex;
        gap: 0.75rem;
        padding: 0.5rem;
        cursor: pointer;
        border-radius: var(--omni-radius-sm);
        transition: background 0.15s;
    }

    .omni-cal-agenda-comic:active {
        background: var(--omni-surface-hover);
    }

    .omni-cal-agenda-thumb {
        width: 40px;
        height: 60px;
        object-fit: cover;
        border-radius: 4px;
        flex-shrink: 0;
    }

    .omni-cal-agenda-info {
        display: flex;
        flex-direction: column;
        justify-content: center;
        min-width: 0;
    }

    .omni-cal-agenda-title {
        font-size: 0.85rem;
        font-weight: 600;
        color: var(--omni-text);
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* The variant suffix carries the only difference between two adjacent
       printings, so it wraps rather than clamps -- an ellipsis here would
       reintroduce exactly the collision this line exists to remove (GH-114). */
    .omni-cal-agenda-variant {
        font-size: 0.7rem;
        font-weight: 500;
        line-height: 1.25;
        color: var(--omni-text-muted);
        white-space: normal;
        overflow-wrap: anywhere;
    }

    .omni-cal-agenda-meta {
        font-size: 0.7rem;
        color: var(--omni-text-muted);
        display: flex;
        gap: 0.5rem;
    }

    .omni-cal-month-label {
        font-size: 1.1rem;
        min-width: auto;
    }
}

/* ── Interested heart pin (dark theme variant) ───────────────────── */
.omni-card-interest-heart {
    position: absolute;
    top: 8px;
    right: 8px;
    z-index: 3;
    width: 32px;
    height: 32px;
    padding: 0;
    background: color-mix(in srgb, var(--omni-overlay-ink) 72%, transparent);
    border: 1px solid color-mix(in srgb, var(--on-image) 18%, transparent);
    border-radius: 50%;
    display: grid;
    place-items: center;
    cursor: pointer;
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
                background 0.2s,
                border-color 0.2s;
}

.omni-card-interest-heart svg {
    width: 16px;
    height: 16px;
    stroke: var(--accent-interest);
    fill: none;
    stroke-width: 2;
    transition: fill 0.2s, stroke 0.2s;
}

.omni-card-interest-heart:hover {
    transform: scale(1.08);
    background: var(--omni-marvel-red);
    border-color: var(--omni-marvel-red);
}

.omni-card-interest-heart:hover svg {
    fill: var(--on-image);
    stroke: var(--on-image);
}

.omni-card-interest-heart.is-active {
    background: var(--omni-marvel-red);
    border-color: var(--omni-marvel-red);
    box-shadow: 0 2px 8px color-mix(in srgb, var(--omni-marvel-red) 50%, transparent);
}

.omni-card-interest-heart.is-active svg {
    fill: var(--on-image);
    stroke: var(--on-image);
}

/* ---- Loading parity with /search: skeleton card, stagger, no-flash refresh ---- */
.omni-card-skeleton {
    cursor: default;
    pointer-events: none;
    --sk-speed: 1.4s;
}
.omni-card-skeleton .omni-card-cover.skeleton-block {
    aspect-ratio: 2 / 3;
    width: 100%;
    animation: shimmer 1.4s var(--ease-standard, ease) infinite;
}
.omni-card .skeleton-line {
    display: block;
    overflow: hidden;
    background: linear-gradient(90deg, var(--surface-2), var(--surface), var(--surface-2));
    background-size: 220% 100%;
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    animation: shimmer 1.4s var(--ease-standard, ease) infinite;
}
.omni-card .skeleton-title { height: .85rem; width: 80%; margin-bottom: .4rem; }
.omni-card .skeleton-title.short { width: 52%; }
.omni-card .skeleton-meta-row { display: flex; gap: .4rem; align-items: center; margin-top: .5rem; }
.omni-card .skel-pill { height: .8rem; width: 3.2rem; border: 1px solid var(--border); }
.omni-card .skeleton-year { height: .7rem; width: 2.6rem; }

/* NO per-card entrance animation here — deliberately.
   Guarded by tests/test_omnis_scroll_reveal_no_entrance.py.

   Cards sit inside `content-visibility: auto` week groups (section 8), so a group's
   subtree is not rendered until scroll reveals it — and CSS animations do not run
   inside skipped content, they START at reveal. A staggered entrance therefore
   stops being a page-load flourish and becomes a mid-scroll replay: measured in
   real Chrome at the reveal frame, 8/8 cards at `opacity: 0` with animations at
   `currentTime: 0` — the group's header painting above an empty grid, then cards
   popping in. Reported as a random flicker while scrolling.

   Restoring an entrance means gating it to the first paint from JS; CSS alone
   cannot say "only on load" about content whose render is deferred. Its removal
   also retires the entrance-vs-`:hover` transform contention that cost a live bug:
   with nothing animating the shell, nothing can outrank the tile lift. */

/* Refresh-don't-flash: dim + desaturate old tiles while new ones load. */
.omni-grid.is-refreshing {
    opacity: .62;
    filter: saturate(.85);
    pointer-events: none;
    transition: opacity var(--motion-med, .32s) var(--ease-standard, ease);
}

@media (prefers-reduced-motion: reduce) {
    .omni-card .skeleton-line { animation: none; }
}

.omni-card-best-price,
.omni-day-best-price {
    color: var(--success);
    font-weight: 800;
}

/* ---- Homepage search (filters the current release window) ---- */
.omni-search {
    position: relative;
    display: flex;
    align-items: center;
    min-width: 0;
    flex: 0 1 15rem;
}

.omni-search-icon {
    position: absolute;
    left: 0.6rem;
    font-size: 0.85rem;
    opacity: 0.6;
    pointer-events: none;
}

.omni-search-input {
    width: 100%;
    padding: 0.3rem 1.9rem 0.3rem 2rem;
    /* Theme-aware translucent pill, not a dark-only fixed overlay: borders
       and fills are mixed from the theme's own border/text tokens (the
       sanctioned pattern in this file's owner group — see shelf.css for
       other examples), so this reads correctly in every theme. Same
       decision applied to the .omni-list rules below. */
    border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
    border-radius: 999px;
    background: color-mix(in srgb, var(--text) 6%, transparent);
    color: inherit;
    font-size: 0.85rem;
    line-height: 1.4;
}

.omni-search-input::placeholder { opacity: 0.55; }

.omni-search-input:focus {
    outline: none;
    border-color: color-mix(in srgb, var(--border) 95%, transparent);
    background: color-mix(in srgb, var(--text) 10%, transparent);
}

/* Suppress the native WebKit clear affordance — we render our own. */
.omni-search-input::-webkit-search-cancel-button { display: none; }

.omni-search-clear {
    position: absolute;
    right: 0.4rem;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.3rem;
    height: 1.3rem;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: inherit;
    opacity: 0.6;
    font-size: 0.7rem;
    cursor: pointer;
}

.omni-search-clear:hover { opacity: 1; }

/* ---- Homepage list view ---- */
.omni-list-wrapper { overflow-x: auto; }
.omni-list-wrapper.is-refreshing { opacity: 0.55; transition: opacity 120ms ease; }

.omni-list {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.88rem;
}

.omni-list thead th {
    position: sticky;
    top: 0;
    z-index: 1;
    padding: 0.5rem 0.6rem;
    text-align: left;
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    opacity: 0.7;
    /* The old undefined-custom-property fallback always won in practice, so
       a light-theme user got a fixed dark sticky header. --surface is the
       real theme-aware token. */
    background: var(--surface);
    border-bottom: 1px solid color-mix(in srgb, var(--border) 90%, transparent);
    white-space: nowrap;
}

.omni-list-sortable { cursor: pointer; user-select: none; }
.omni-list-sortable:hover { opacity: 1; }
.omni-list-sortable:focus-visible,
.omni-list-title-link:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: -2px;
}

.omni-list-group-header th {
    padding: 0.7rem 0.6rem 0.35rem;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    opacity: 0.75;
    background: transparent;
    border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
}

/* The header's fold control. A transparent, full-width button so the whole
   header row is the hit target, the way the grid's .omni-foc-header div is —
   but a real <button>, which brings native focus and Enter/Space for free.
   Typography is inherited from the <th> rather than restated, so the two
   shapes cannot drift apart; only the count opts out of the uppercase
   tracking, since "12 TITLES" reads as a heading rather than a tally.
   Hover lifts the <th>'s own opacity rather than recolouring the label: the
   grid tints its label with a per-mode accent (--omni-accent-upcoming vs
   --omni-accent-mine, keyed on a .mode-* class the list wrapper does not
   carry), and a single hardcoded accent here would be wrong in My List. */
.omni-list-group-header th:hover { opacity: 1; }

.omni-list-group-toggle {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    padding: 0;
    background: none;
    border: 0;
    color: inherit;
    font: inherit;
    letter-spacing: inherit;
    text-transform: inherit;
    text-align: left;
    cursor: pointer;
    user-select: none;
}

.omni-list-group-toggle:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

.omni-list-group-count {
    text-transform: none;
    letter-spacing: 0;
    opacity: 0.8;
}

/* Borders mix from the theme border token; fills and hover tints mix from the
   theme text token — same alpha weights as before the token fix, just mixed
   through the theme instead of a fixed dark-only channel (matches
   .omni-search-input above, and the color-mix convention used across the
   rest of this codebase — see shelf.css for other examples). */
.omni-list-row { cursor: pointer; border-bottom: 1px solid color-mix(in srgb, var(--border) 45%, transparent); }
.omni-list-row:hover { background: color-mix(in srgb, var(--text) 4%, transparent); }
/* Publisher-tinted hover, matching the grid tile's glow. A separate rule rather
   than a `var(--pub-glow, var(--text))` fallback inside the rule above, because
   the two need different weights: 4% is tuned for --text (a near-black or
   near-white) and is invisible at the saturation of a brand hue. */
.omni-list-row.pub-glow:hover { background: color-mix(in srgb, var(--pub-glow) 10%, transparent); }
.omni-list-row td { padding: 0.45rem 0.6rem; vertical-align: middle; }

.omni-list-cover-col, .omni-list-cover { width: 40px; }
.omni-list-cover img { width: 32px; height: auto; border-radius: 2px; display: block; }
.omni-list-title { font-weight: 600; line-height: 1.25; }
.omni-list-title-link { display: block; color: inherit; text-decoration: none; }
.omni-list-mobile-meta { display: none; }

.omni-list-heart {
    border: 0;
    background: transparent;
    padding: 0.2rem;
    cursor: pointer;
    color: inherit;
    opacity: 0.5;
}
.omni-list-heart svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; }
.omni-list-heart.is-active { opacity: 1; color: var(--accent-interest); }
.omni-list-heart.is-active svg { fill: currentColor; }

/* Narrow viewports: collapse to cover + stacked meta + heart, matching /search. */
@media (max-width: 780px) {
    .omni-list thead th:not(.omni-list-cover-col):not(:nth-child(2)):not(.omni-list-action-col),
    .omni-list-row td:nth-child(n+3):not(.omni-list-action) {
        display: none;
    }
    .omni-list-mobile-meta {
        display: flex;
        flex-wrap: wrap;
        gap: 0.45rem;
        margin-top: 0.15rem;
        font-size: 0.78rem;
        opacity: 0.7;
    }
}

/* The week's editorial picks, lifted to the front of "This week". Deliberately
   quiet: a brand-tinted edge and a small kicker, never a separate strip. The
   kicker's words stay on the muted ink token; only the icon carries brand. */
.omni-card-shell.is-highlight > .omni-card {
    border-color: color-mix(in srgb, var(--brand) 55%, var(--omni-border));
}
.omni-card-pick {
    display: flex;
    align-items: center;
    gap: 0.3rem;
    font-size: 0.65rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    color: var(--omni-text-muted);
    margin-bottom: 0.25rem;
}
.omni-card-pick .bi {
    color: var(--brand);
}
