
/* ==== css/theme.css ==== */
/* Theme Variables - Simplified Color System */
/* Colors are loaded dynamically from /api/theme.css */

html {
    scrollbar-gutter: stable;
    --dashboard-bg-opacity: 1;

    /*
     * Two tokens the stylesheets ask for and no theme supplies.
     *
     * A var() with no fallback that resolves to nothing makes the whole
     * declaration invalid, so these did not degrade -- they vanished. The
     * transitions in status.css simply never ran, and the finder search mode,
     * which is coloured to tell it apart from the other modes, inherited the
     * colour of whatever it sat in and so looked like no mode at all.
     *
     * accent-secondary is mixed from the accent the theme does define rather
     * than picked: it has to differ from --accent-primary to do its job, and
     * it must not borrow --accent-warning or --accent-success, whose colours
     * already mean something. Pulling the accent towards the text colour
     * gives a second tone of the same family on every theme.
     */
    --accent-secondary: color-mix(in srgb, var(--accent-primary) 55%, var(--text-primary));
    --transition-normal: 0.2s ease;
}

/* Background Patterns */
html[data-theme] body {
    background-color: var(--background-primary);
    color: var(--text-primary);
    isolation: isolate;
}

html[data-theme] body:not(.no-background-dots)::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background-image: radial-gradient(var(--background-dots) 1px, transparent 1px);
    background-size: 15px 15px;
}

body.no-background-dots::before {
    content: none;
}

/* Custom Background (auto / gradient / image) — ::after keeps opacity on the backdrop only */
body.has-custom-background::after {
    content: "";
    position: fixed;
    /* A blurred edge fades to transparent, which shows as a frame. Reaching
       past the viewport by twice the blur radius keeps the frame off screen. */
    inset: calc(var(--bd-blur, 0px) * -2);
    z-index: -1;
    pointer-events: none;
    /* The tint is the theme's own page colour laid over the picture, so it
       reads as the theme turning the picture down rather than as a grey film.
       Backdrop tuning (Settings.BackdropTuning) sets the four --bd- values. */
    background-image:
        linear-gradient(
            color-mix(in srgb, var(--background-primary) calc(var(--bd-tint, 0) * 100%), transparent),
            color-mix(in srgb, var(--background-primary) calc(var(--bd-tint, 0) * 100%), transparent)),
        var(--custom-background-image);
    background-size: cover;
    filter: blur(var(--bd-blur, 0px)) brightness(var(--bd-brightness, 1)) saturate(var(--bd-saturate, 1));
    opacity: var(--dashboard-bg-opacity, 1);
}

body.has-custom-background.bg-image::after {
    background-position: center center;
    background-repeat: no-repeat;
}

/* Preview icons in config */
.theme-icon-preview .preview-icon {
    width: 28px;
    height: 28px;
    display: inline-block;
    margin-right: 8px;
    border-radius: var(--radius-4, 6px);
    background: linear-gradient(135deg, rgba(255,255,255,0.06), rgba(0,0,0,0.06));
}

/* Themed icon wrappers and variants */
.icon-themed, .icon-themed img, .icon-themed .preview-icon {
    transition: filter .15s ease, opacity .15s ease, background-color .15s ease;
    border-radius: var(--radius-4, 6px);
}
.icon-themed--muted img, .icon-themed--muted .preview-icon {
    filter: grayscale(calc(var(--icon-theme-intensity, 0.6))); 
    opacity: calc(1 - (var(--icon-theme-intensity, 0.25) * 0.8));
}
.icon-themed--tinted img, .icon-themed--tinted .preview-icon {
    filter: saturate(calc(0.6 + var(--icon-theme-intensity, 0.4)));
    mix-blend-mode: multiply;
}
.icon-themed--tinted {
    background-color: color-mix(in srgb, var(--accent-primary) var(--icon-theme-intensity, 0.12), transparent);
}
/* A glyph is its own host — there is no <img> inside a category header's emoji
   — so the variants above, which reach for img/.preview-icon, would leave it at
   full colour beside harmonised favicons. Tinted deliberately skips the multiply
   blend the image rule uses: on text it eats the glyph rather than colouring it. */
.icon-themed--muted.icon-themed-glyph {
    filter: grayscale(calc(var(--icon-theme-intensity, 0.6)));
    opacity: calc(1 - (var(--icon-theme-intensity, 0.25) * 0.8));
}
.icon-themed--tinted.icon-themed-glyph {
    filter: saturate(calc(0.6 + var(--icon-theme-intensity, 0.4)));
}

.icon-themed--overlay {
    padding: 3px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background-color: color-mix(in srgb, var(--accent-primary) var(--icon-theme-intensity, 0.12), transparent);
}


/* ── Focus styles (keyboard navigation) ── */

*:focus {
    outline: none;
}

input:focus-visible,
textarea:focus-visible,
select:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: 0;
}

button:focus-visible,
a:focus-visible,
[role="button"]:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
}

/* A block's title with keyboard focus: the selected row's tint, like the
   rows under it, rather than a ring of its own. */
.category-title:focus-visible {
    outline: none;
    background: var(--bookmark-row-selected-bg);
    border-radius: var(--layout-radius-sm, 8px);
}

.search-command-group-header:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: -2px;
}

/* No Animations Option */
body.no-animations * {
    animation: none !important;
    transition: none !important;
    transform: none !important;
}

/* ==== css/spacing-tokens.css ==== */
/* Spacing tokens — the single scale for padding/margin/gap across the
   dashboard.

   Unscoped: these apply everywhere. They outlived a second, layout-scoped
   token layer that only one of two layouts could reach; there is one layout
   now and this is the only scale.

   Two groups:

   1. The macro scale — steps on a 0.25rem (4px) rhythm, for spacing where no
      other value nearby carries a different meaning. Loaded ahead of every
      view stylesheet so every declaration that adopts var(--space-N, …)
      resolves to the token, with the existing literal kept as the fallback.

      --space-1-5 breaks the strict 0.25rem rhythm on purpose: 0.35rem is not
      an outlier, it is the single most common spacing value in the app —
      roughly 120 uses across dashboard.css, config-view.css, health-view.css
      and dashboard-inbox.css combined, more than any other value including
      0.25rem and 0.5rem. Rounding it onto the strict rhythm would have been
      the single largest visual change in this whole pass, applied at the
      widest scale, to the value the app already leans on hardest. Naming it
      instead of erasing it is the more honest read of what the CSS is
      already doing.

   2. A separate, deliberately finer density sub-scale for the three bookmark
      row densities (comfortable/compact/dense). These do NOT collapse onto
      the macro scale: the three-way distinction between them is the entire
      point of the setting, and 0.25rem steps are too coarse to hold three
      readable levels apart (0.35rem and 0.2rem, the current compact/dense
      values, are both closer to the same macro step than to each other).
      Named here so the relationship between the six values is visible in one
      place instead of scattered across three separate rules, without
      pretending they are three cases of one number. */

:root {
    /* ── Macro scale ─────────────────────────────────────────────────── */
    --space-hairline: 0.15rem;
    --space-1: 0.25rem;
    --space-1-5: 0.35rem; /* deliberate exception — see note above */
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-5: 1.5rem;
    --space-6: 2rem;
    --space-7: 3rem;

    /* ── Bookmark row density sub-scale ──────────────────────────────── */
    /* Vertical padding inside a row. */
    --density-row-pad-comfortable: 0.5rem;
    --density-row-pad-compact: 0.35rem;
    --density-row-pad-dense: 0.2rem;

    /* Gap between rows in a list. */
    --density-row-list-gap-comfortable: 0.42rem;
    --density-row-list-gap-compact: 0.3rem;
    --density-row-list-gap-dense: 0.18rem;

    /* Gap between the elements inside one row (favicon / name / shortcut /
       response time). Only "dense" narrows this today. */
    --density-row-inner-gap-dense: 0.45rem;
}

/* ==== css/typography-tokens.css ==== */
/* Typography tokens — the scale for font-size across the dashboard.

   Unscoped, same reasoning as spacing-tokens.css: classic is the default
   layout, so a token layer classic cannot reach helps almost nobody.

   Anchored on the actual distribution rather than a clean textbook ratio.
   Measured across every static/css/*.css file: 365 font-size declarations
   using rem, spread over 40 distinct values between 0.46rem and 1.65rem —
   most of them within a couple hundredths of a rem of their neighbour.
   0.72rem alone accounts for 65 of the 365 — more than any other single
   value — with 0.68rem (45) and 0.7rem (30) immediately next to it.

   Seven steps, each placed at the densest point of a real cluster in that
   distribution, not at a round mathematical interval:

     --text-2xs   0.65rem   the smallest meta text (absorbs 0.58–0.68rem)
     --text-xs    0.72rem   labels, shortcuts, status text (absorbs 0.7–0.76rem)
     --text-sm    0.8rem    secondary body text (absorbs 0.78–0.85rem)
     --text-base  0.9rem    primary small body text (absorbs 0.86–0.95rem)
     --text-md    1rem      the app's base size (absorbs 0.98–1.05rem)
     --text-lg    1.1rem    small headings, emphasised values (absorbs 1.1–1.15rem)
     --text-xl    1.25rem   section headings (absorbs 1.25–1.32rem)

   A handful of values fell outside every cluster (0.46–0.54rem and
   1.35–1.65rem, 14 declarations total) and were left as literals rather than
   forced onto the nearest step — those are few enough, and far enough from
   their neighbours, to read as deliberate rather than accidental. */

:root {
    --text-2xs: 0.65rem;
    --text-xs: 0.72rem;
    --text-sm: 0.8rem;
    --text-base: 0.9rem;
    --text-md: 1rem;
    --text-lg: 1.1rem;
    --text-xl: 1.25rem;
}

/* ==== css/radius-tokens.css ==== */
/* Radius tokens — the scale for border-radius across the dashboard.

   Unscoped, same reasoning as spacing-tokens.css and typography-tokens.css.

   A layout-scoped layer used to define four steps of its own (8/12/16px and
   a pill) for one of the two layouts. Four is coarser than what the app
   actually uses, and there is one layout now.
   Measured across every static/css/*.css file: 352 border-radius
   declarations, mixing px and rem for the same handful of intended sizes
   (3px sitting next to 0.2rem, 4px next to 0.25rem, and so on), clustering
   around eight real steps rather than four:

     --radius-1     2px    barely-rounded corners (chips, thin borders)
     --radius-2     3px
     --radius-3     4px    the single largest cluster in the app
     --radius-4     6px    the second-largest cluster
     --radius-5     8px    matches --layout-radius-sm in the modern layer
     --radius-6     10px
     --radius-7     12px   matches --layout-radius-md in the modern layer
     --radius-pill  999px  fully round — already used consistently (82
                           uses, already all 999px); named for the same
                           reason the others are, not because it needed
                           consolidating

   Deliberately in px, not rem: border-radius does not need to scale with
   the user's font-size setting the way spacing and type do, and every
   existing literal in the codebase already mixes both units for the same
   intended size — a single unit removes that mismatch outright.

   One declaration (20px, in responsive.css) sat 8px from the nearest step
   and was left as a literal rather than forced onto --radius-7. */

:root {
    --radius-1: 2px;
    --radius-2: 3px;
    --radius-3: 4px;
    --radius-4: 6px;
    --radius-5: 8px;
    --radius-6: 10px;
    --radius-7: 12px;
    --radius-pill: 999px;
}

/* ==== css/layer-tokens.css ==== */
/* Layer tokens — the z-index scale for stacking *within* one view's own
   scrolling column: a sticky band pinned to the top of the viewport, and row
   content that has to rise above it while transiently active.

   Unscoped, same reasoning as spacing-tokens.css and radius-tokens.css: this
   has nothing to do with the modern-layout-only tokens in
   layout-modern-tokens.css, and every layout needs it. Loaded ahead of every
   stylesheet that reads a --layer-* value (feed-row.css,
   dashboard-multi-select.css, list-view-shell.css).

   This is deliberately NOT the overlay scale (the 1000s/2000s/12000s spread
   across dashboard.css, modal.css, search.css, dashboard-inbox.css and
   friends). Those overlays are appended to <body> and already sit far above
   anything named here — nothing in this file needs to, or should, compete
   with them. This scale is for elements that stack against each other
   *inside* a view's own content column, where the numbers were 1, 5 and 20
   with nothing tying them together: a plain bump on the highest of the three
   would have worked once and collided again the next time something sticky
   was added.

   --layer-sticky-band: the view's own pinned chrome. list-view-shell.css's
     .lvs-header and dashboard-multi-select.css's .multi-select-toolbar (the
     bulk-select bar) both sit here — both `position: sticky; top: 0`, both
     painted above the row list beneath them, never above each other.

   --layer-row-raised: a row wins this only while something of its own has to
     rise above the sticky band above — right now that is a row with its
     overflow menu open (feed-row.css). Must beat --layer-sticky-band, not
     just a sibling row's default z-index: auto — see feed-row.css for why a
     depth theme's backdrop-filter makes that necessary. The menu itself
     (health-view.css, z-index: 20) stays put: it only ever has to beat
     content inside its own row's stacking context, which a bump here does
     not touch. */
:root {
    --layer-sticky-band: 5;
    --layer-row-raised: 6;
}

/* ==== css/action-btn-tokens.css ==== */
/* Action-button tokens — shared look for icon-only card actions and the
   bottom dock buttons, across every theme.
 *
 * Same color-mix-against-text-primary formula as --hairline/--accent-wash in
 * theme-depth.css, so no per-theme or dark-mode override is needed: mixing
 * against --text-primary tracks light/dark automatically.
 */
:root {
    --action-btn-bg: color-mix(in srgb, var(--text-primary) 6%, transparent);
    --action-btn-bg-hover: color-mix(in srgb, var(--text-primary) 12%, transparent);
    --action-btn-color: var(--text-secondary);
    --action-btn-shadow: 0 1px 2px color-mix(in srgb, var(--text-primary) 10%, transparent);
}

/* ==== css/theme-depth.css ==== */
/* Depth tokens — a second layer, derived from the thirteen a theme declares.

   A theme is thirteen colours (see ThemeColors in models.go), of which two are
   surfaces: background-primary and background-secondary. Two surfaces cannot
   express depth, which is why every view ends up flat — a card, a panel and a
   control are all painted the same, and only a border says where one stops and
   the next begins.

   Rather than ask every theme for more colours, this derives them. Three
   reasons that is the right trade:

     - There are 148 built-in themes. Adding a field means editing all of them,
       plus every custom theme somebody already made, plus a migration for
       colors.json.
     - A theme's colours cannot hold a gradient anyway: sanitizeCSSColor
       (security.go) accepts hex, rgb(), hsl() and colour names, so a
       linear-gradient() in a theme field arrives as "transparent".
     - layout-modern-tokens.css already works this way and has proven it holds
       up across the whole set.

   Nothing here paints anything on its own. These are tokens for view CSS to
   consume, so this file can land without changing a single pixel, and each
   surface can be moved over one at a time. */

/* ── Depth scale ────────────────────────────────────────────────────────────
   The two scalars every token below is built from, set per depth choice.

   On <body> rather than <html> because that is where data-depth lands, and a
   variable cannot read an attribute from an element below it. Everything
   derived from them lives on body too, so a descendant inherits both. */

/* Deze drie stonden als `body[data-depth="flat"] body` -- een afstammelings-
   selector, die een body binnen een body vraagt en dus nooit matcht. Alleen de
   kale `body` hierboven raakte iets, waardoor elke installatie op tint 3% en
   diepte 1 bleef staan en de keuze in Instellingen niets deed, welke stand je
   ook koos. Nu op body zelf, met html[data-theme] ervoor om boven de kale
   `body` uit te komen. */

body {
    --theme-tint: 3%;
    --theme-depth: 1;
}

html[data-theme] body[data-glow="off"] {
    --glow-strength: 0;
}

html[data-theme] body[data-glow="soft"] {
    --glow-strength: 0.6;
}

html[data-theme] body[data-glow="full"] {
    --glow-strength: 1;
}

html[data-theme] body[data-depth="flat"] {
    --theme-tint: 0%;
    --theme-depth: 0;
}

html[data-theme] body[data-depth="soft"] {
    --theme-tint: 3%;
    --theme-depth: 1;
}

html[data-theme] body[data-depth="rich"] {
    --theme-tint: 5%;
    --theme-depth: 1.5;
}

/* De trede boven rich, waar het archetype van een theme helemaal opengaat.
   Glass staat er niet boven maar ernaast: dat is een soort en geen trede --
   je kijkt er doorheen, wat met diepte niets te maken heeft. */

html[data-theme] body[data-depth="vivid"] {
    --theme-tint: 7%;
    --theme-depth: 2;
}

/* ── Effecten ────────────────────────────────────────────────────────────
   De rem op alles wat een archetype tekent. Elk karaktertoken vermenigvuldigt
   ermee, net zoals ze al met --theme-depth doen, dus uit is echt uit en er is
   geen tweede plek waar een effect zich kan verstoppen.

   Niet hetzelfde als diepte. Diepte zegt hoeveel van het theme áchter de
   inhoud getekend wordt; effecten zeggen hoe hard het archetype spreekt. Een
   plat theme met volle effecten bestaat niet -- diepte 0 maakt alles nul --
   maar een diep theme met effecten uit wel, en dat is precies de lezer die
   de diepte wil en de glans niet. */

html[data-theme] body {
    --theme-effects: 1;
}

html[data-theme] body[data-effects="off"] {
    --theme-effects: 0;
}

html[data-theme] body[data-effects="held"] {
    --theme-effects: 0.6;
}

html[data-theme] body[data-effects="full"] {
    --theme-effects: 1;
}

html[data-theme] body {
    /*
     * How much of the theme's own colour bleeds into its greys.
     *
     * This is what separates a palette that looks designed from one that looks
     * like grey with coloured text on it: in Catppuccin, Tokyo Night and Night
     * Owl the backgrounds are not neutral, they are heavily desaturated
     * versions of the accent. Three percent is enough to read as intent and
     * little enough to keep contrast where it was.
     *
     * Set above per depth choice, and overridden per theme at the bottom of
     * this file: a paper theme wants 0, a neon one can carry more.
     */

    /*
     * The surface ladder.
     *
     * Mixed with text-primary rather than with white or black, because
     * text-primary is the one colour that moves the right way in both light
     * and dark themes: toward the reader, away from the page. A hard-coded
     * rgba(255,255,255,…) would lighten a light theme into nothing.
     *
     * surface-0 is the page. Each step above it is a card on a card.
     */
    --surface-0: var(--background-primary);
    /* The 3/6/9 are scaled by --theme-surface-step, which the server derives
       from how much room a palette has between its page and its ink: three per
       cent of an ink that sits close to its ground is not a step anyone can
       see. The depth control still multiplies on top, so the reader's choice
       is unchanged; this only decides what one rung is worth on this theme. */
    --surface-1: color-mix(in srgb,
        var(--accent-primary) var(--theme-tint),
        color-mix(in srgb, var(--text-primary) calc(3% * var(--theme-depth) * var(--theme-surface-step, 1)), var(--background-primary)));
    --surface-2: color-mix(in srgb,
        var(--accent-primary) var(--theme-tint),
        color-mix(in srgb, var(--text-primary) calc(6% * var(--theme-depth) * var(--theme-surface-step, 1)), var(--background-primary)));
    --surface-3: color-mix(in srgb,
        var(--accent-primary) var(--theme-tint),
        color-mix(in srgb, var(--text-primary) calc(9% * var(--theme-depth) * var(--theme-surface-step, 1)), var(--background-primary)));

    /*
     * Hairlines.
     *
     * border-primary is used at full opacity today, which draws a line around
     * everything and makes a dashboard read as a form. At 45% it separates
     * without announcing itself; the strong variant is for what should be
     * announced.
     */
    --hairline: color-mix(in srgb, var(--border-primary) 45%, transparent);
    --hairline-strong: color-mix(in srgb, var(--border-primary) 70%, transparent);

    /*
     * The lit top edge of a raised surface. One inset line, and a flat
     * rectangle starts reading as something sitting on the page rather than
     * printed on it. This is the cheapest depth cue there is.
     */
    /* The line itself, kept separate so theme-character.css can fold the
       ambient glow ring in behind it without restating the strength. It did
       restate it, at 6%, and quietly won -- so raising the number here moved
       nothing at all. One percentage, one place. */
    --edge-top: inset 0 1px 0 color-mix(in srgb,
        var(--text-primary) calc((16% + 30% * var(--theme-sheen, 0)) * var(--theme-depth)), transparent);
    /*
     * And the same light down the left edge, at two thirds.
     *
     * A lit top edge alone is a highlight with no source; a second line down
     * the side puts the light somewhere -- top left, where every interface
     * since the first one has kept it -- and the surface stops reading as a
     * rectangle with a stripe on it. Weaker than the top, because a face
     * turned away from the light is what makes the direction legible.
     */
    --edge-side: inset 1px 0 0 color-mix(in srgb,
        var(--text-primary) calc(10% * var(--theme-depth)), transparent);
    --edge-light: var(--edge-top), var(--edge-side);

    /*
     * And the shaded bottom edge, which is the other half of the same cue.
     *
     * A lit line on its own is a highlight; a lit line with a shaded one
     * opposite it is a slab with a thickness, which is what every surface in
     * the redesign drafts wears. Nine percent rather than six: a shadow at
     * the foot of a surface reads weaker than a highlight at its head, so
     * matching the numbers would not match the effect.
     *
     * Mixed from --text-primary like its twin, never from black. This line
     * lies ON the surface, so a fixed black one turns into a hard stripe on
     * every light and paper theme -- a cast shadow may use black, a surface
     * line may not.
     */
    --edge-dark: inset 0 -1px 0 color-mix(in srgb,
        var(--text-primary) calc(24% * var(--theme-depth)), transparent);

    /*
     * The state bloom: what the reader is acting on, right now.
     *
     * Not to be confused with --surface-glow-ring in theme-character.css,
     * which is an ambient halo under resting surfaces and is set per theme.
     * This one is tight and bright and appears only on focus and selection.
     * The two must stay tellable apart -- once two effects look alike, they
     * stop meaning two things, which is why this blurs at 12px where the
     * ambient ring blurs at 18px and up, and carries no offset or spread.
     *
     * Decoration only. The focus outline stays exactly where it was, so a
     * reader who cannot make out the glow loses nothing by it.
     *
     * Scaled by --theme-depth, so flat resolves it to a fully transparent
     * colour and paints nothing.
     */
    /*
     * How much of both glows is drawn, as a multiplier the reader sets.
     *
     * The strength lives here rather than in each glow, so "quieter" is one
     * number for the state bloom and the ambient ring alike -- and a theme that
     * asks for no glow at all still paints none, because zero times anything is
     * zero. 1 is what shipped, 0.6 is the quiet middle, and the default is 0:
     * an effect that ships on is one every reader has to find the switch for.
     */
    --glow-strength: 0;

    --bloom: 0 0 12px color-mix(in srgb,
        var(--accent-primary) calc(40% * var(--theme-depth) * var(--glow-strength, 0)), transparent);

    /*
     * And the shadow the surface casts.
     *
     * The two edges are one pixel each, and on a card that already draws a
     * hairline border they lose that argument -- which is why switching depth
     * changed almost nothing you could point at. A surface reads as lifted
     * because of what falls under it, not because of a line on it.
     *
     * Black rather than --text-primary: this falls BESIDE the surface, on the
     * page, where black is what a shadow is on any theme. The inset edges are
     * the opposite case and stay theme-derived.
     */
    --surface-cast: 0 calc(2px * var(--theme-depth)) calc(10px * var(--theme-depth))
        color-mix(in srgb, black calc(22% * var(--theme-depth)), transparent);

    /*
     * A very slight vertical wash for a card to carry. Two and a half percent:
     * invisible when you look for it, and the reason a large surface stops
     * looking like a swatch.
     */
    --surface-sheen: var(--surface-grain), var(--surface-gloss), linear-gradient(
        180deg,
        color-mix(in srgb, var(--text-primary) calc(2.5% * var(--theme-depth)), transparent) 0%,
        transparent 40%
    );

    /*
     * Gloss: the light a lacquered surface catches.
     *
     * A theme's `sheen` (0-1, handlers.go) sets it; zero draws nothing, so
     * every theme that never mentions it looks exactly as before. Two layers:
     * a bright band across the top, and a soft highlight at the top left where
     * the lit edges already put the light. White rather than --text-primary:
     * a reflection is the colour of the light, not of the ink, and on a light
     * theme it still reads as a shine on the surface.
     */
    /*
     * De glans schaalt niet mee tot boven soft.
     *
     * Elke andere laag hier vermenigvuldigt met --theme-depth, en bij vivid
     * is dat 2. Voor een schaduw of een tint is dat precies de bedoeling;
     * voor wit over een heel paneel niet -- 15% × sheen 1 × 2 is 30% wit, en
     * dan kijk je niet meer naar het palet maar naar de glans. Afgekapt op
     * 1.15: dieper mag alles worden, feller niet.
     */
    --gloss-depth: min(var(--theme-depth), 1.15);

    --surface-gloss:
        radial-gradient(120% 70% at 18% 0%,
            color-mix(in srgb, white calc(7% * var(--theme-sheen, 0) * var(--gloss-depth) * var(--theme-effects, 1)), transparent) 0%,
            transparent 62%),
        linear-gradient(180deg,
            color-mix(in srgb, white calc(9% * var(--theme-sheen, 0) * var(--gloss-depth) * var(--theme-effects, 1)), transparent) 0%,
            color-mix(in srgb, white calc(2.5% * var(--theme-sheen, 0) * var(--gloss-depth) * var(--theme-effects, 1)), transparent) 42%,
            transparent 58%);

    /*
     * Grain: de herhalende glans van een geborsteld of geweven vlak.
     *
     * Twee lagen, de tweede haaks op de eerste en op 40% -- dat verschil is
     * precies wat borstel van weefsel scheidt. Borstel draait op een lage
     * sterkte, waar die tweede laag onder een procent wit uitkomt en dus niet
     * te zien is; weefsel draait vol en krijgt zijn kruising.
     *
     * De hoek en de sterkte komen van de server (--theme-grain-angle en
     * --theme-grain-scale, zie theme_archetype.go). Sterkte nul tekent niets,
     * en dat is elk theme dat er niet om vraagt.
     *
     * Bewust een verloop en geen ruisplaatje: een data-URI met feTurbulence
     * over elk vlak kost op elk paneel, en levert minder op dan deze twee
     * regels.
     */
    --surface-grain:
        repeating-linear-gradient(
            var(--theme-grain-angle, 0deg),
            color-mix(in srgb, white calc(3.5% * var(--theme-grain-scale, 0) * var(--gloss-depth) * var(--theme-effects, 1)), transparent) 0px,
            color-mix(in srgb, white calc(3.5% * var(--theme-grain-scale, 0) * var(--gloss-depth) * var(--theme-effects, 1)), transparent) 1px,
            transparent 1px,
            transparent 3px),
        repeating-linear-gradient(
            calc(var(--theme-grain-angle, 0deg) + 90deg),
            color-mix(in srgb, white calc(1.4% * var(--theme-grain-scale, 0) * var(--gloss-depth) * var(--theme-effects, 1)), transparent) 0px,
            color-mix(in srgb, white calc(1.4% * var(--theme-grain-scale, 0) * var(--gloss-depth) * var(--theme-effects, 1)), transparent) 1px,
            transparent 1px,
            transparent 3px);

    /* The same light on a control under the pointer. */
    --gloss-hover: linear-gradient(180deg,
        color-mix(in srgb, white calc(11% * var(--theme-sheen, 0) * var(--gloss-depth) * var(--theme-effects, 1)), transparent) 0%,
        transparent 70%);

    /*
     * Accent washes, for what is selected, hovered or currently true. Named by
     * strength rather than by use, so a badge and a row can share one scale.
     */
    --accent-wash-weak: color-mix(in srgb, var(--accent-primary) 8%, transparent);
    --accent-wash: color-mix(in srgb, var(--accent-primary) 16%, transparent);
    --accent-wash-strong: color-mix(in srgb, var(--accent-primary) 28%, transparent);
}

/*
 * Contrast comes first.
 *
 * Every mix above moves a text background, and the tint moves it toward a
 * colour rather than along the grey axis — which is exactly the direction that
 * costs contrast on a light theme. A reader who has asked for more contrast is
 * asking for the flat version.
 */
@media (prefers-contrast: more) {
    html[data-theme] body {
        --theme-tint: 0%;
        --theme-depth: 0.5;
        --hairline: var(--border-primary);
        --hairline-strong: var(--border-primary);

        /* En om dezelfde reden geen gloed. Een accentgekleurde halo is precies
           de kleurbeweging die deze voorkeur weg wil hebben. Dat geldt net zo
           voor de toestandsbloom: de focus-outline draagt de betekenis al. */
        --theme-surface-glow: 0;
        --glow-strength: 0;
        --bloom: none;
    }
}

/* Per-theme character ------------------------------------------------------
   Only the themes whose whole point is a texture the default would flatten or
   overstate. Everything else inherits the values above. */

/* Paper is paper: no tint, and barely any lift. */
html[data-theme="paper-ink-light"] body,
html[data-theme="paper-ink-dark"] body,
html[data-theme="bone-china-light"] body,
html[data-theme="bone-china-dark"] body,
html[data-theme="salt-flat-light"] body,
html[data-theme="salt-flat-dark"] body {
    --theme-tint: 0%;
    --theme-depth: 0.4;
}

/* A CRT and a neon grid are the one place where more is the point. */
html[data-theme="neon-grid-dark"] body,
html[data-theme="midnight-neon-dark"] body,
html[data-theme="retro-crt-dark"] body,
html[data-theme="cold-cathode-dark"] body {
    --theme-tint: 6%;
    --theme-depth: 1.4;
}

/* Monochrome themes have no hue to bleed; tinting them would invent one. */
html[data-theme="monochrome-mist-dark"] body,
html[data-theme="monochrome-mist-light"] body,
html[data-theme="graphite-prism-dark"] body,
html[data-theme="graphite-prism-light"] body {
    --theme-tint: 0%;
}

/* ── Painting ──────────────────────────────────────────────────────────────
   Everything above is definitions. Below is the first set of surfaces moved
   over to them.

   Kept in this file rather than spread through the view stylesheets, so the
   whole visual change is one <link> in dashboard.html: remove it and the
   dashboard is byte-for-byte what it was. When this settles it belongs behind
   an Appearance setting (Flat / Soft / Rich) rather than behind a link tag. */

/*
 * Two accent washes behind the page.
 *
 * Only defined here. They are drawn by .theme-backdrop-layer
 * (theme-backdrop.css), together with the theme's own backdrop: ::before draws
 * the dot grid and ::after belongs to the custom background, so the washes
 * have a layer of their own that survives both.
 *
 * Large, soft and low: at 7% and 5% this is not a visible gradient, it is the
 * page no longer being one flat rectangle. The warning colour in the second
 * one is deliberate — a single hue looks like a mistake, two related hues look
 * like light.
 */
html[data-theme] body:not([data-depth="flat"]) {
    /*
     * Een variabele en geen background-image: de laag zet de hele stapel
     * (wolken onder, theme-achtergrond erboven) in één regel, en staat hier
     * alleen nog waar hij uit bestaat.
     */
    --depth-wash:
        radial-gradient(65rem 45rem at 12% -12%,
            color-mix(in srgb, var(--accent-primary) calc(7% * var(--theme-depth)), transparent),
            transparent 70%),
        radial-gradient(55rem 38rem at 96% 6%,
            color-mix(in srgb, var(--accent-warning) calc(5% * var(--theme-depth)), transparent),
            transparent 70%);

}

/*
 * The card presets, on the ladder.
 *
 * Both already mixed a surface by hand — 92% and 94% of the page colour with a
 * little text mixed in — which is the same idea as surface-2 and surface-1
 * without the tint or a name. Swapping them keeps the two presets distinct and
 * gives them the theme's own colour, the lit top edge, and a border that stops
 * outlining every card at full strength.
 */
body:not([data-depth="flat"]) .dashboard-grid.layout-cards .category {
    background-color: var(--surface-2);
    background-image: var(--surface-sheen);
    border: 1px solid var(--hairline);
    box-shadow: var(--edge-light), var(--edge-dark), var(--surface-cast);
}

body:not([data-depth="flat"]) .dashboard-grid.layout-widgets .category {
    background-color: var(--surface-1);
    background-image: var(--surface-sheen);
    border: 1px solid var(--hairline);
    box-shadow: var(--edge-light), var(--edge-dark), var(--surface-cast);
}

/*
 * The widget body.
 *
 * Deliberately off the ladder. surface-2 lifted every tile a step above the
 * card it sits in, and on every theme that read as a grey panel pasted on the
 * page. The tile keeps its page-coloured ground from dashboard.css; depth gives it a
 * hairline and the soft shadow, not the lit edges.
 *
 * The gloss is a different thing from the ladder, and it belongs here. It is a
 * gradient over whatever ground the tile already has -- the same one
 * .config-panel carries below -- so it lights the surface without lifting it a
 * step. Without it a widget was the one card-shaped surface in the app that
 * stayed matte while every panel in Config had a sheen on it.
 */
body:not([data-depth="flat"]) .dashboard-widget .dashboard-widget-body {
    border: 1px solid var(--hairline);
    box-shadow: var(--surface-cast);
    background-image: var(--surface-grain), var(--surface-gloss);
}

/*
 * A modal keeps its own background.
 *
 * background-modal is deliberately translucent and the modern layout blurs what
 * is behind it; replacing that with an opaque surface would trade a good effect
 * for a smaller one. What it gets from the ladder is the edge — the lit top
 * line and a border that has stopped being a full-strength outline.
 */
body:not([data-depth="flat"]) .modal {
    border: 1px solid var(--hairline-strong);
    box-shadow:
        var(--edge-light),
        var(--edge-dark),
        0 24px 64px rgba(0, 0, 0, 0.28),
        0 2px 8px rgba(0, 0, 0, 0.12);
}

/* ── Selection, scrollbars ─────────────────────────────────────────────────
   Two things no theme has ever touched, both visible on every screen. */

/*
 * Selected text was browser blue on all 148 themes.
 *
 * Nothing in the stylesheets ever set ::selection, so a reader dragging over a
 * bookmark name got the operating system's idea of a highlight on top of a
 * carefully chosen palette. This is one rule and it applies everywhere,
 * including at flat: it is not depth, it is a colour the theme already owns.
 */
html[data-theme] ::selection {
    background: color-mix(in srgb, var(--accent-primary) 30%, transparent);
    color: var(--text-primary);
}

/*
 * The same argument for scrollbars. A few view stylesheets styled their own;
 * the page itself did not, so the one piece of chrome that is on screen the
 * whole time stayed the browser default.
 *
 * scrollbar-color is the standard property and covers Firefox and current
 * Chromium; the -webkit rules below carry the ones that do not have it yet.
 */
/*
 * --hairline-strong is defined on body, and the page's own scrollbar is styled
 * from html, where it does not exist: the property was invalid there, fell back
 * to the browser's scrollbar, and with "always show scrollbars" that drew a
 * white strip down the right edge of every dark theme. The same mix, from the
 * border colour html does have, as the fallback; the track in the page colour.
 */
html[data-theme] {
    --page-scrollbar-thumb: var(--hairline-strong, color-mix(in srgb, var(--border-primary) 70%, transparent));
    scrollbar-color: var(--page-scrollbar-thumb) var(--background-primary);
    scrollbar-width: thin;
}

html[data-theme] ::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}

html[data-theme] ::-webkit-scrollbar-track {
    background: transparent;
}

html[data-theme]::-webkit-scrollbar-track {
    background: var(--background-primary);
}

html[data-theme] ::-webkit-scrollbar-thumb {
    background: var(--hairline-strong, var(--page-scrollbar-thumb));
    border-radius: var(--radius-pill, 999px);
    /* Inset by a border rather than by width, so the thumb keeps the full
       track's hit area while looking narrower than it is. */
    border: 3px solid transparent;
    background-clip: padding-box;
}

html[data-theme] ::-webkit-scrollbar-thumb:hover {
    background: color-mix(in srgb, var(--accent-primary) 45%, var(--border-primary));
    background-clip: padding-box;
}

/* ── The backdrop pattern ──────────────────────────────────────────────────
   The dot grid was the only texture in the app, and it was the same texture in
   all of them. It is two things now: what a theme asks for, and what the reader
   asks for instead.

   Left alone (the "auto" default), a theme picks the texture that suits it — a
   blueprint gets a grid, a CRT gets rules, paper gets nothing. Choose anything
   else in Appearance and that wins everywhere, on every theme.

   The line patterns are drawn weaker than the dots deliberately: see
   --theme-pattern-line below. */

html[data-theme] body {
    --theme-pattern-image: radial-gradient(var(--background-dots) 1px, transparent 1px);
    --theme-pattern-size: 15px 15px;
    /*
     * Line patterns are drawn from a weakened copy of the dot colour.
     *
     * background-dots was picked to read as a sparse speckle. A rule or a hatch
     * at that same value puts twenty times as much ink on the page and starts
     * competing with the text — legible on a dark theme, heavy on a light one.
     * Forty-five percent is where a texture stays a texture.
     */
    --theme-pattern-line: color-mix(in srgb, var(--background-dots) 45%, transparent);
}

html[data-theme] body:not(.no-background-dots)::before {
    background-image: var(--theme-pattern-image);
    background-size: var(--theme-pattern-size);
}

/* ── What a theme asks for ─────────────────────────────────────────────────
   Only where the texture is part of what the theme is. Everything not named
   here keeps the dots. These lose to an explicit choice, which carries one more
   attribute and therefore more specificity — order in this file is not what
   decides it. */

/* A ruled grid, for the themes that are drawings. */
html[data-theme="blueprint-dark"] body,
html[data-theme="blueprint-light"] body,
html[data-theme="graphite-prism-dark"] body,
html[data-theme="graphite-prism-light"] body {
    --theme-pattern-image:
        linear-gradient(var(--theme-pattern-line) 1px, transparent 1px),
        linear-gradient(90deg, var(--theme-pattern-line) 1px, transparent 1px);
    --theme-pattern-size: 32px 32px, 32px 32px;
}

/* Horizontal rules, for the ones pretending to be a screen. Eight pixels, not
   four: at four it moirés on a HiDPI display and the page shimmers as it
   scrolls. */
/* Both halves of each family: these four named only their dark variant, so
   switching one of them to light quietly dropped the scanlines and left the
   theme looking like every other. A family is one idea whichever way round it
   is printed. */
html[data-theme="retro-crt-dark"] body,
html[data-theme="retro-crt-light"] body,
html[data-theme="neon-grid-dark"] body,
html[data-theme="neon-grid-light"] body,
html[data-theme="cold-cathode-dark"] body,
html[data-theme="cold-cathode-light"] body,
html[data-theme="static-noise-dark"] body,
html[data-theme="static-noise-light"] body {
    --theme-pattern-image: linear-gradient(var(--theme-pattern-line) 1px, transparent 1px);
    --theme-pattern-size: 100% 8px;
}

/* Diagonal hatch, widely spaced, for the paper and fabric ones. */
html[data-theme="sumi-ink-dark"] body,
html[data-theme="sumi-ink-light"] body,
html[data-theme="moss-stone-dark"] body,
html[data-theme="moss-stone-light"] body,
html[data-theme="desert-rose-dark"] body,
html[data-theme="desert-rose-light"] body {
    --theme-pattern-image: repeating-linear-gradient(
        45deg,
        var(--theme-pattern-line) 0 1px,
        transparent 1px 14px
    );
    --theme-pattern-size: auto;
}

/* Dots, for the misty, watery and cool ones: fog, glass, ice, deep water.
   Named rather than left to the default above, so the intent is on the page
   for every family instead of a quarter of them getting it by omission. */
html[data-theme="arctic-cyan-dark"] body,
html[data-theme="arctic-cyan-light"] body,
html[data-theme="cerulean-skylark-dark"] body,
html[data-theme="cerulean-skylark-light"] body,
html[data-theme="coral-reef-dark"] body,
html[data-theme="coral-reef-light"] body,
html[data-theme="deep-lagoon-dark"] body,
html[data-theme="deep-lagoon-light"] body,
html[data-theme="dusk-horizon-dark"] body,
html[data-theme="dusk-horizon-light"] body,
html[data-theme="forest-everglade-dark"] body,
html[data-theme="forest-everglade-light"] body,
html[data-theme="forest-moss-dark"] body,
html[data-theme="forest-moss-light"] body,
html[data-theme="frosted-juniper-dark"] body,
html[data-theme="frosted-juniper-light"] body,
html[data-theme="glacier-mint-dark"] body,
html[data-theme="glacier-mint-light"] body,
html[data-theme="harbour-fog-dark"] body,
html[data-theme="harbour-fog-light"] body,
html[data-theme="hermetic-teal-dark"] body,
html[data-theme="hermetic-teal-light"] body,
html[data-theme="iceberg-drift-dark"] body,
html[data-theme="iceberg-drift-light"] body,
html[data-theme="iris-meadow-dark"] body,
html[data-theme="iris-meadow-light"] body,
html[data-theme="kelp-drift-dark"] body,
html[data-theme="kelp-drift-light"] body,
html[data-theme="lavender-mist-dark"] body,
html[data-theme="lavender-mist-light"] body,
html[data-theme="monochrome-mist-dark"] body,
html[data-theme="monochrome-mist-light"] body,
html[data-theme="nordic-frost-dark"] body,
html[data-theme="nordic-frost-light"] body,
html[data-theme="ocean-depth-dark"] body,
html[data-theme="ocean-depth-light"] body,
html[data-theme="owl-hours-dark"] body,
html[data-theme="owl-hours-light"] body,
html[data-theme="pale-night-dark"] body,
html[data-theme="pale-night-light"] body,
html[data-theme="pastel-mountain-dark"] body,
html[data-theme="pastel-mountain-light"] body,
html[data-theme="porcelain-blue-dark"] body,
html[data-theme="porcelain-blue-light"] body,
html[data-theme="rose-pine-dark"] body,
html[data-theme="rose-pine-light"] body,
html[data-theme="sea-glass-dark"] body,
html[data-theme="sea-glass-light"] body,
html[data-theme="storm-petrel-dark"] body,
html[data-theme="tidepool-lens-dark"] body,
html[data-theme="tidepool-lens-light"] body,
html[data-theme="champagne-flute-dark"] body,
html[data-theme="champagne-flute-light"] body,
html[data-theme="prism-veil-dark"] body,
html[data-theme="prism-veil-light"] body,
html[data-theme="rain-window-dark"] body,
html[data-theme="rain-window-light"] body,
html[data-theme="lime-soda-dark"] body,
html[data-theme="lime-soda-light"] body,
html[data-theme="rosewater-pane-dark"] body,
html[data-theme="rosewater-pane-light"] body,
html[data-theme="gloss-cinnabar-dark"] body,
html[data-theme="gloss-cinnabar-light"] body,
html[data-theme="gloss-jade-dark"] body,
html[data-theme="gloss-jade-light"] body,
html[data-theme="gloss-aubergine-dark"] body,
html[data-theme="gloss-aubergine-light"] body,
html[data-theme="gloss-butterscotch-dark"] body,
html[data-theme="gloss-butterscotch-light"] body,
html[data-theme="borealis-veil-dark"] body,
html[data-theme="borealis-veil-light"] body,
html[data-theme="solar-wind-dark"] body,
html[data-theme="solar-wind-light"] body,
html[data-theme="nebula-nursery-dark"] body,
html[data-theme="nebula-nursery-light"] body,
html[data-theme="polar-dawn-dark"] body,
html[data-theme="polar-dawn-light"] body,
html[data-theme="hoarfrost-dark"] body,
html[data-theme="hoarfrost-light"] body,
html[data-theme="frozen-lavender-dark"] body,
html[data-theme="frozen-lavender-light"] body,
html[data-theme="frost-fern-dark"] body,
html[data-theme="frost-fern-light"] body,
html[data-theme="winter-ember-dark"] body,
html[data-theme="winter-ember-light"] body,
html[data-theme="storm-petrel-light"] body {
    --theme-pattern-image: radial-gradient(var(--background-dots) 1px, transparent 1px);
    --theme-pattern-size: 15px 15px;
}

/* Grid, for the technical ones: circuitry, editors, steel and machinery. */
html[data-theme="andromeda-drift-dark"] body,
html[data-theme="andromeda-drift-light"] body,
html[data-theme="cherry-graphite-dark"] body,
html[data-theme="cherry-graphite-light"] body,
html[data-theme="cobalt-ink-dark"] body,
html[data-theme="cobalt-ink-light"] body,
html[data-theme="commit-grey-dark"] body,
html[data-theme="commit-grey-light"] body,
html[data-theme="copper-circuit-dark"] body,
html[data-theme="copper-circuit-light"] body,
html[data-theme="cosmic-editor-dark"] body,
html[data-theme="cosmic-editor-light"] body,
html[data-theme="denim-fade-dark"] body,
html[data-theme="denim-fade-light"] body,
html[data-theme="editor-default-dark"] body,
html[data-theme="editor-default-light"] body,
html[data-theme="emerald-matrix-dark"] body,
html[data-theme="emerald-matrix-light"] body,
html[data-theme="foundry-iron-dark"] body,
html[data-theme="foundry-iron-light"] body,
html[data-theme="licorice-layer-dark"] body,
html[data-theme="licorice-layer-light"] body,
html[data-theme="midnight-ink-dark"] body,
html[data-theme="midnight-ink-light"] body,
html[data-theme="moonlit-steel-dark"] body,
html[data-theme="moonlit-steel-light"] body,
html[data-theme="obsidian-gold-dark"] body,
html[data-theme="obsidian-gold-light"] body,
html[data-theme="oceanic-steel-dark"] body,
html[data-theme="oceanic-steel-light"] body,
html[data-theme="polar-night-dark"] body,
html[data-theme="polar-night-light"] body,
html[data-theme="slate-one-dark"] body,
html[data-theme="slate-one-light"] body,
html[data-theme="smoked-plum-dark"] body,
html[data-theme="smoked-plum-light"] body,
html[data-theme="steel-dawn-dark"] body,
html[data-theme="steel-dawn-light"] body,
html[data-theme="thunderhead-dark"] body,
html[data-theme="thunderhead-light"] body,
html[data-theme="tidal-slate-dark"] body,
html[data-theme="tidal-slate-light"] body,
html[data-theme="tomorrow-dusk-dark"] body,
html[data-theme="tomorrow-dusk-light"] body,
html[data-theme="vampire-castle-dark"] body,
html[data-theme="vampire-castle-light"] body,
html[data-theme="velocity-ink-dark"] body,
html[data-theme="velocity-ink-light"] body,
html[data-theme="volcanic-ash-dark"] body,
html[data-theme="kevlar-weave-dark"] body,
html[data-theme="kevlar-weave-light"] body,
html[data-theme="graphite-twill-dark"] body,
html[data-theme="graphite-twill-light"] body,
html[data-theme="race-livery-dark"] body,
html[data-theme="race-livery-light"] body,
html[data-theme="forged-titanium-dark"] body,
html[data-theme="forged-titanium-light"] body,
html[data-theme="aluminium-deck-dark"] body,
html[data-theme="aluminium-deck-light"] body,
html[data-theme="gunmetal-bronze-dark"] body,
html[data-theme="gunmetal-bronze-light"] body,
html[data-theme="rose-titanium-dark"] body,
html[data-theme="rose-titanium-light"] body,
html[data-theme="satin-nickel-dark"] body,
html[data-theme="satin-nickel-light"] body,
html[data-theme="volcanic-ash-light"] body {
    --theme-pattern-image:
        linear-gradient(var(--theme-pattern-line) 1px, transparent 1px),
        linear-gradient(90deg, var(--theme-pattern-line) 1px, transparent 1px);
    --theme-pattern-size: 32px 32px, 32px 32px;
}

/* Scanlines, for the ones that glow: neon, signal, phosphor, electric. */
html[data-theme="absinthe-dark"] body,
html[data-theme="absinthe-light"] body,
html[data-theme="bio-abyss-dark"] body,
html[data-theme="bio-abyss-light"] body,
html[data-theme="candy-pop-dark"] body,
html[data-theme="candy-pop-light"] body,
html[data-theme="chartreuse-static-dark"] body,
html[data-theme="chartreuse-static-light"] body,
html[data-theme="city-lights-dark"] body,
html[data-theme="city-lights-light"] body,
html[data-theme="electric-orchid-dark"] body,
html[data-theme="electric-orchid-light"] body,
html[data-theme="fox-night-dark"] body,
html[data-theme="fox-night-light"] body,
html[data-theme="horizon-glow-dark"] body,
html[data-theme="horizon-glow-light"] body,
html[data-theme="jungle-neon-dark"] body,
html[data-theme="jungle-neon-light"] body,
html[data-theme="midnight-firefly-dark"] body,
html[data-theme="midnight-firefly-light"] body,
html[data-theme="midnight-neon-dark"] body,
html[data-theme="midnight-neon-light"] body,
html[data-theme="peacock-dark"] body,
html[data-theme="peacock-light"] body,
html[data-theme="retro-groove-dark"] body,
html[data-theme="retro-groove-light"] body,
html[data-theme="royal-amethyst-dark"] body,
html[data-theme="royal-amethyst-light"] body,
html[data-theme="signal-flare-dark"] body,
html[data-theme="signal-flare-light"] body,
html[data-theme="solar-ember-dark"] body,
html[data-theme="solar-ember-light"] body,
html[data-theme="sunflower-ink-dark"] body,
html[data-theme="sunflower-ink-light"] body,
html[data-theme="synth-sunset-dark"] body,
html[data-theme="synth-sunset-light"] body,
html[data-theme="terminal-amber-dark"] body,
html[data-theme="terminal-amber-light"] body,
html[data-theme="tyrian-dark"] body,
html[data-theme="tyrian-light"] body,
html[data-theme="ultraviolet-dark"] body,
html[data-theme="ultraviolet-light"] body,
html[data-theme="violet-shades-dark"] body,
html[data-theme="violet-shades-light"] body,
html[data-theme="vivid-hyper-dark"] body,
html[data-theme="matrix-rain-dark"] body,
html[data-theme="matrix-rain-light"] body,
html[data-theme="matrix-redpill-dark"] body,
html[data-theme="matrix-redpill-light"] body,
html[data-theme="matrix-bluepill-dark"] body,
html[data-theme="matrix-bluepill-light"] body,
html[data-theme="matrix-construct-dark"] body,
html[data-theme="matrix-construct-light"] body,
html[data-theme="vivid-hyper-light"] body {
    --theme-pattern-image: linear-gradient(var(--theme-pattern-line) 1px, transparent 1px);
    --theme-pattern-size: 100% 8px;
}

/* Hatch, for paper, cloth, earth and anything made by hand. */
html[data-theme="bamboo-panda-dark"] body,
html[data-theme="bamboo-panda-light"] body,
html[data-theme="bone-china-dark"] body,
html[data-theme="bone-china-light"] body,
html[data-theme="candlelit-study-dark"] body,
html[data-theme="candlelit-study-light"] body,
html[data-theme="desert-sand-dark"] body,
html[data-theme="desert-sand-light"] body,
html[data-theme="ember-ash-dark"] body,
html[data-theme="ember-ash-light"] body,
html[data-theme="great-wave-dark"] body,
html[data-theme="great-wave-light"] body,
html[data-theme="library-mahogany-dark"] body,
html[data-theme="library-mahogany-light"] body,
html[data-theme="marigold-dusk-dark"] body,
html[data-theme="marigold-dusk-light"] body,
html[data-theme="mirage-sand-dark"] body,
html[data-theme="mirage-sand-light"] body,
html[data-theme="mulberry-silk-dark"] body,
html[data-theme="mulberry-silk-light"] body,
html[data-theme="olive-drab-dark"] body,
html[data-theme="olive-drab-light"] body,
html[data-theme="oxblood-leather-dark"] body,
html[data-theme="oxblood-leather-light"] body,
html[data-theme="paper-ink-dark"] body,
html[data-theme="paper-ink-light"] body,
html[data-theme="patina-verdigris-dark"] body,
html[data-theme="patina-verdigris-light"] body,
html[data-theme="pistachio-cream-dark"] body,
html[data-theme="pistachio-cream-light"] body,
html[data-theme="rhubarb-tart-dark"] body,
html[data-theme="rhubarb-tart-light"] body,
html[data-theme="rusted-rail-dark"] body,
html[data-theme="rusted-rail-light"] body,
html[data-theme="saffron-robe-dark"] body,
html[data-theme="saffron-robe-light"] body,
html[data-theme="sakura-night-dark"] body,
html[data-theme="sakura-night-light"] body,
html[data-theme="salt-flat-dark"] body,
html[data-theme="salt-flat-light"] body,
html[data-theme="solar-flats-dark"] body,
html[data-theme="solar-flats-light"] body,
html[data-theme="tarnished-brass-dark"] body,
html[data-theme="tarnished-brass-light"] body,
html[data-theme="terracotta-studio-dark"] body,
html[data-theme="terracotta-studio-light"] body,
html[data-theme="wheat-field-dark"] body,
html[data-theme="wheat-field-light"] body,
html[data-theme="zen-ember-dark"] body,
html[data-theme="indigo-letterpress-dark"] body,
html[data-theme="indigo-letterpress-light"] body,
html[data-theme="iron-gall-dark"] body,
html[data-theme="iron-gall-light"] body,
html[data-theme="squid-ink-dark"] body,
html[data-theme="squid-ink-light"] body,
html[data-theme="vermilion-seal-dark"] body,
html[data-theme="vermilion-seal-light"] body,
html[data-theme="zen-ember-light"] body {
    --theme-pattern-image: repeating-linear-gradient(
        45deg,
        var(--theme-pattern-line) 0 1px,
        transparent 1px 14px
    );
    --theme-pattern-size: auto;
}

/* ── What the reader asks for ──────────────────────────────────────────────
   Anything but "auto" overrides whatever the theme wanted, on every theme. */

html[data-theme] body[data-pattern="dots"] {
    --theme-pattern-image: radial-gradient(var(--background-dots) 1px, transparent 1px);
    --theme-pattern-size: 15px 15px;
}

html[data-theme] body[data-pattern="grid"] {
    --theme-pattern-image:
        linear-gradient(var(--theme-pattern-line) 1px, transparent 1px),
        linear-gradient(90deg, var(--theme-pattern-line) 1px, transparent 1px);
    --theme-pattern-size: 32px 32px, 32px 32px;
}

html[data-theme] body[data-pattern="lines"] {
    --theme-pattern-image: linear-gradient(var(--theme-pattern-line) 1px, transparent 1px);
    --theme-pattern-size: 100% 8px;
}

html[data-theme] body[data-pattern="hatch"] {
    --theme-pattern-image: repeating-linear-gradient(
        45deg,
        var(--theme-pattern-line) 0 1px,
        transparent 1px 14px
    );
    --theme-pattern-size: auto;
}

/* Nothing at all, without touching the dots toggle's other behaviour. */
html[data-theme] body[data-pattern="none"]::before {
    background-image: none;
}

/* ── A backdrop is texture enough ──────────────────────────────────────────
   The pattern was the only texture once; now every theme draws a backdrop,
   and dots on top of bokeh or mountains read as a screen door. Left to the
   theme ("auto"), the pattern steps aside while a backdrop is on screen and
   comes back when it is switched off. A pattern the reader picked still
   draws, over anything. */
html[data-theme] body[data-pattern="auto"][data-theme-backdrop="on"]::before {
    background-image: none;
}

/* Reduced transparency hides the backdrop layer, so the theme's own pattern
   is all the texture left and comes back. */
@media (prefers-reduced-transparency: reduce) {
    html[data-theme] body[data-pattern="auto"][data-theme-backdrop="on"]:not(.no-background-dots)::before {
        background-image: var(--theme-pattern-image);
    }
}

/* ── Status colours as a family ────────────────────────────────────────────
   accent-success, -warning and -error are used at full saturation for every
   badge and pill, which is right for a dot and loud for a filled shape. These
   are the quiet halves of the same three colours, for backgrounds and borders,
   so the health view can carry a hundred rows without shouting a hundred
   times. */

html[data-theme] body {
    --success-wash: color-mix(in srgb, var(--accent-success) 14%, transparent);
    --success-edge: color-mix(in srgb, var(--accent-success) 45%, transparent);
    --warning-wash: color-mix(in srgb, var(--accent-warning) 14%, transparent);
    --warning-edge: color-mix(in srgb, var(--accent-warning) 45%, transparent);
    --error-wash: color-mix(in srgb, var(--accent-error) 14%, transparent);
    --error-edge: color-mix(in srgb, var(--accent-error) 45%, transparent);
}

/* ── Grain ─────────────────────────────────────────────────────────────────
   Large soft gradients band on 8-bit displays: the wash behind the page shows
   as rings rather than as a wash. Two percent of noise over the top breaks the
   steps up, and gives every surface a texture that a flat fill cannot have.

   On body::after, which is free unless a custom background is set — that rule
   in theme.css owns the same pseudo-element, and its backdrop is the reader's
   own choice rather than something to sprinkle over. */

html[data-theme] body:not(.has-custom-background):not([data-depth="flat"])::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    opacity: calc(0.022 * var(--theme-depth));
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ── The category header ───────────────────────────────────────────────────
   On the default preset a category has no surface at all — it is a title and a
   list, and the glow behind the page is the only thing that changed for the
   readers who use it. This gives the title a start without turning the list
   into a card: a rule under it that carries the theme's colour and fades. */

body:not([data-depth="flat"]) .dashboard-grid:not(.layout-cards):not(.layout-widgets) .category-title {
    border-bottom: 1px solid transparent;
    border-image: linear-gradient(
        90deg,
        color-mix(in srgb, var(--accent-primary) 55%, transparent) 0%,
        var(--hairline) 35%,
        transparent 85%
    ) 1;
    padding-bottom: var(--space-1-5, 0.35rem);
}

/* A category spanning more than one column says so with its rule.
 *
 * The rule above fades out at 85%, which is right for a single column: the
 * title names the category and the line trails off under it. Across two
 * columns that same fade lands well short of the edge, so a wide category
 * looked like a narrow one whose line happened to be long -- the width was
 * only legible from the badge in the header, or by counting the bookmarks.
 *
 * Here it carries further and holds its colour longer, so the eye follows it
 * to where the category actually ends. Still a hairline, still fading rather
 * than stopping: the point is to trace the span, not to draw a box around it.
 */
body:not([data-depth="flat"]) .dashboard-grid:not(.layout-cards):not(.layout-widgets) .category.category--wide .category-title {
    border-image: linear-gradient(
        90deg,
        color-mix(in srgb, var(--accent-primary) 70%, transparent) 0%,
        color-mix(in srgb, var(--accent-primary) 30%, var(--hairline)) 45%,
        var(--hairline) 80%,
        transparent 100%
    ) 1;
}

/* ── Typographic character ─────────────────────────────────────────────────
   A theme is a palette today. These two variables let it also be a voice.

   Deliberately narrow: the font stack stays whatever the reader chose in
   Appearance, because a theme silently replacing a chosen typeface is a theme
   overstepping. What a theme may do is the spacing and the case of the one
   element that names it — the category title — which is where a terminal reads
   as a terminal and a library reads as a library. */

html[data-theme] body {
    --theme-title-spacing: normal;
    --theme-title-transform: none;
}

body:not([data-depth="flat"]) .category-title-label {
    letter-spacing: var(--theme-title-spacing);
    text-transform: var(--theme-title-transform);
}

/* The terminals: wide, upper, like a status line. */
html[data-theme="retro-crt-dark"] body,
html[data-theme="retro-crt-light"] body,
html[data-theme="terminal-amber-dark"] body,
html[data-theme="terminal-amber-light"] body,
html[data-theme="neon-grid-dark"] body,
html[data-theme="cold-cathode-dark"] body,
html[data-theme="static-noise-dark"] body,
html[data-theme="static-noise-light"] body {
    --theme-title-spacing: 0.09em;
    --theme-title-transform: uppercase;
}

/* The printed ones: a little air, no shouting. */
html[data-theme="paper-ink-dark"] body,
html[data-theme="paper-ink-light"] body,
html[data-theme="library-mahogany-dark"] body,
html[data-theme="library-mahogany-light"] body,
html[data-theme="sumi-ink-dark"] body,
html[data-theme="sumi-ink-light"] body,
html[data-theme="bone-china-dark"] body,
html[data-theme="bone-china-light"] body {
    --theme-title-spacing: 0.02em;
}

/*
 * Gloss on the surfaces that are not cards.
 *
 * Only the gloss layer, not the base wash: these surfaces never carried the
 * 2.5% sheen, and a theme without `sheen` must draw them exactly as it did.
 */
body:not([data-depth="flat"]) :is(
    .config-panel,
    .lvs-summary,
    .lvs-drawer-frame,
    .search-container
) {
    background-image: var(--surface-grain), var(--surface-gloss);
}

/* The dock and the plated header controls are plates of their own. */
body:not([data-depth="flat"]):is([data-action-bar="bottom"], [data-action-bar="left"], [data-action-bar="right"])
    .dashboard-section.section-controls .header-shortcuts,
body:not([data-depth="flat"]):not([data-header-buttons="plain"]) .dashboard-section.section-controls .header-destinations :is(
    .dashboard-link-anchor,
    .library-link-anchor,
    .config-link-anchor,
    .page-nav-btn--inbox
) {
    background-image: var(--surface-grain), var(--surface-gloss);
}

body:not([data-depth="flat"]) :is(.config-btn, .dashboard-widget-health-row, .dashboard-widget-trend-row):hover,
body:not([data-depth="flat"]):is([data-action-bar="bottom"], [data-action-bar="left"], [data-action-bar="right"])
    .dashboard-section.section-controls .header-shortcuts .search-button:hover {
    background-image: var(--gloss-hover);
}

/* ==== css/theme-ink.css ==== */
/* Inkt — de zwakkere tekstkleuren afgeleid van het vlak waar ze op liggen.

   Een theme levert dertien platte kleuren, waarvan drie tekst. Die drie zijn
   gekozen tegen background-primary en worden daarna nooit bijgesteld.
   theme-depth.css leidt uit diezelfde dertien een surfaceladder af -- surface-1
   tot surface-3, elk een stap lichter dan de pagina -- en dat is de goede
   beweging, maar hij gaat maar een kant op: de vlakken schuiven omhoog, de inkt
   blijft staan. Elke laag kost dus contrast, en niemand ziet het aankomen, want
   tegen de pagina gemeten klopt elk theme.

   Gemeten over de 107 donkere built-ins, met de surfaces zoals theme-depth.css
   ze bij data-depth="rich" afleidt:

     tertiaire tekst op surface-3               33 van de 107 onder 3,0:1
     tertiaire tekst op surface-2               25 van de 107
     tertiaire tekst op background-secondary    15 van de 107
     primaire en secundaire tekst op de pagina   0 van de 107

   Slechtste geval Cosmic Editor [dark] op 1,4:1, dan Slate One op 1,7:1.

   Mengen richting text-primary helpt, maar loopt vast zodra een theme zelf al
   weinig contrast heeft: Slate One komt zo niet verder dan 2,5:1. Wat wel werkt
   is afleiden -- neem de lichtheid van het vlak, zet er een vaste afstand
   bovenop, houd tint en verzadiging van datzelfde vlak vast. Contrast wordt
   daarmee een eigenschap van het systeem in plaats van van het palet.

   Dit bestand overschrijft --text-secondary en --text-tertiary op <body>. De
   generated CSS zet ze op <html>, dus de palet-waarden blijven bestaan en
   blijven bereikbaar; alles binnen body krijgt de afgeleide versie. Geen
   wijziging nodig in de 229 plekken die deze twee tokens al gebruiken, geen
   migratie van colors.json, geen aanpassing aan een van de 214 palettes.
   --text-primary blijft ongemoeid: die haalde overal al ruim 4,5:1 en draagt de
   eigen kleur van het theme.

   Doorgerekend in internal/app/theme_ink_test.go, dat faalt zodra een theme
   onder de vloer zakt. */

@supports (color: oklch(from white l c h)) {

    html[data-theme] body {
        /*
         * Het vlak waar de inkt van wordt afgeleid.
         *
         * surface-2 en niet de pagina, want de pagina is het gunstigste geval
         * en niet het geval dat stukloopt. Static Noise [dark] laat zien
         * waarom: pagina #000000, maar surface-3 komt op #2B2B2B uit. Inkt die
         * op zwart is afgesteld haalt daar 2,4:1, hoe hard je de afstand ook
         * opdraait. Vanaf surface-2 gerekend haalt datzelfde theme 4,8:1, en
         * tekst die tóch op de kale pagina staat krijgt gratis wat meer.
         *
         * surface-2 volgt bovendien de diepte-instelling: op data-depth="flat"
         * vallen de surfaces samen met de pagina, en dan klopt het anker weer
         * precies, zonder dat hier iets voor nodig is.
         */
        --surf: var(--surface-2, var(--background-primary));

        /*
         * De afstand in OKLCH-lichtheid tussen inkt en vlak.
         *
         * Gekalibreerd over alle 214 built-ins, licht en donker, gemeten op
         * elke laag waar de tekst kan belanden -- surface-0 tot surface-3 en
         * background-secondary -- en bij zowel data-depth="soft" als "rich".
         * Bij 0.44 haalt het slechtste theme 4,68:1 en de mediaan 5,0:1, met
         * een spreiding over de hele bibliotheek van nog geen punt.
         *
         * Lager kan als hierarchie belangrijker is dan de norm: 0.34 zet alles
         * boven 3,0:1. Hoger laat tertiaire tekst zo hard meedoen dat het
         * verschil met primair wegvalt. De lezer kan hem verzetten in
         * Instellingen; die waarde komt als inline style op <body> binnen en
         * wint van de default hier. Het bereik staat in normalizeInkGap
         * (models.go) en loopt van 0.30 tot 0.58.
         *
         * 0.47 en geen 0.44: het donkere thema springt van #0e1312 naar
         * #1F2937 op zijn bovenste vlak, en daar haalde de tertiaire inkt nog
         * maar 4,05:1 -- onder de AA-grens van 4,5. Dit is de kleinste stap
         * die elk meegeleverd thema op elk vlak boven die grens tilt (4,56:1
         * in het slechtste geval); TestDerivedInkClearsContrastFloor rekent
         * het na.
         */
        --ink-gap-3: 0.47;

        /*
         * Secundair loopt vast acht honderdsten voor op tertiair, zodat de
         * volgorde blijft kloppen als de lezer aan de schuif draait: de twee
         * treden schuiven samen op en houden hun onderlinge afstand. Alleen
         * --ink-gap-3 wordt doorgegeven vanuit de instellingen; deze volgt.
         */
        --ink-gap-2: calc(var(--ink-gap-3) + 0.08);
    }

    /*
     * De richting waarin de inkt van het vlak af beweegt: in een donker theme
     * omhoog, in een licht theme omlaag. --ink-dir komt per theme uit
     * renderThemeCSSBlock (handlers.go), berekend uit de helderheid van
     * background-primary, zodat het ook klopt voor een zelfgemaakt theme dat
     * geen -dark of -light in zijn naam heeft.
     *
     * De chroma wordt geschaald, nooit opgehoogd. Een opgeteld beetje kleur
     * leek aardig tot het op de grijstinten-themes werd losgelaten: de tint van
     * een neutrale kleur is onbepaald en komt als 0 binnen, dus Static Noise
     * kreeg er een rode zweem bij. Schalen houdt neutraal neutraal en laat een
     * getint vlak zijn tint doorgeven.
     */
    html[data-theme] body,
    html[data-theme] [data-ink-surface] {
        --text-secondary: oklch(from var(--surf)
            clamp(0, calc(l + var(--ink-gap-2) * var(--ink-dir, 1)), 1)
            calc(c * 0.5) h);
        --text-tertiary: oklch(from var(--surf)
            clamp(0, calc(l + var(--ink-gap-3) * var(--ink-dir, 1)), 1)
            calc(c * 0.6) h);
    }

    /*
     * De haak voor stap 2 van het voorstel.
     *
     * Een container die niet op surface-2 maar ergens anders staat, zet
     * data-ink-surface en krijgt daarmee zijn eigen inkt -- de declaraties
     * hierboven staan mee in die selector, want custom properties worden
     * gesubstitueerd op het element waar ze gedeclareerd zijn, en overerven
     * alleen is dus niet genoeg.
     *
     * Nog nergens gebruikt: het gaat hier eerst om de vloer, niet om de
     * fijnregeling. Maar het contract staat er, zodat een view die overgaat
     * alleen een attribuut hoeft te zetten.
     */
    [data-ink-surface="page"]    { --surf: var(--background-primary); }
    [data-ink-surface="raised"]  { --surf: var(--surface-2, var(--background-secondary)); }
    [data-ink-surface="overlay"] { --surf: var(--surface-3, var(--background-modal)); }
}

/* ==== css/theme-backdrop.css ==== */
/* Achtergrond per theme.

   Elk theme heeft er een, en geen twee zijn hetzelfde: themeBackdropImage
   (handlers.go) kiest de vorm uit de theme-id en de kleuren uit het palet van
   dat theme, en zet het resultaat in --theme-backdrop. Hier wordt het alleen
   nog opgehangen.

   Op een eigen element, .theme-backdrop-layer, het eerste kind van body. Niet op
   body zelf en niet op een pseudo-element: ::before tekent het stippenraster
   (theme.css), ::after is van de eigen achtergrond van de gebruiker en van de
   dieptewas (theme-depth.css), en een filter op body zou de inhoud meenemen. De
   laag is vast ten opzichte van het venster, dus de achtergrond blijft staan
   terwijl de bookmarks eroverheen scrollen -- een achtergrond die meescrolt
   verandert van een sfeer in een plaatje.

   De stapeling van onder naar boven:

     background-color   de vlakke themekleur (body)
     laag, z-index -2   deze achtergrond, met de dieptewolken eronder
     ::before           het stippen- of rasterpatroon
     ::after            de eigen achtergrond van de gebruiker, of de dieptewas
     inhoud

   Een eigen achtergrond wint dus altijd: die ligt er letterlijk overheen. */

.theme-backdrop-layer {
    display: none;
}

html[data-theme] body > .theme-backdrop-layer {
    display: block;
    position: fixed;

    /*
     * Een vervaagde rand loopt uit naar doorzichtig en staat als een lijst om
     * het venster. Twee keer de blurstraal voorbij het venster houdt die rand
     * buiten beeld.
     */
    inset: calc(var(--bd-blur, 0px) * -2);
    z-index: -2;
    pointer-events: none;

    /* De themekleur die de gebruiker er met Tint overheen legt (BackdropTuning). */
    --bd-tint-fill: color-mix(in srgb, var(--background-primary) calc(var(--bd-tint, 0) * 100%), transparent);

    /*
     * Van boven naar onder: tint, de theme-achtergrond, de twee accentwolken van
     * theme-depth.css. Allemaal, en in die volgorde: de wolken zijn de diepte van
     * de pagina en de achtergrond is het karakter van het theme, en het een is
     * geen vervanging van het ander.
     *
     * De lijsten voor size en position zijn even lang als de lagen
     * (themeBackdropImage zet ze zo): per laag een eigen tegel en plek.
     */
    background-image:
        linear-gradient(var(--bd-tint-fill), var(--bd-tint-fill)),
        var(--theme-backdrop, none),
        var(--depth-wash, none);
    background-repeat: repeat;
    background-size: auto, var(--theme-backdrop-size, cover), auto, auto;
    background-position: 0 0, var(--theme-backdrop-position, 0 0), 0 0, 0 0;

    /* Blur, helderheid en verzadiging gelden voor alles wat de laag tekent. De
       eigen afbeelding van de gebruiker (theme.css) krijgt dezelfde waarden. */
    filter: blur(var(--bd-blur, 0px)) brightness(var(--bd-brightness, 1)) saturate(var(--bd-saturate, 1));
}

/* Uit is uit: alleen de dieptewolken, zonder overgang die je op het moment van
   omschakelen als een flits ziet. */
html[data-theme] body[data-theme-backdrop="off"] > .theme-backdrop-layer {
    background-image:
        linear-gradient(var(--bd-tint-fill), var(--bd-tint-fill)),
        var(--depth-wash, none);
    background-size: auto, auto, auto;
    background-position: 0 0, 0 0, 0 0;
}

/*
 * data-depth="flat" is de stand waarin een lezer heeft gezegd dat hij geen
 * lagen wil. De achtergrond blijft: hij hoort bij het theme, en flat gaat over
 * diepte, niet over kleur.
 *
 * Wat hier wél weg mag is de dieptewas: dat zijn de lagen waar flat over gaat.
 * Bij depth 0 rekenen die wolken zich toch al tot volledig doorzichtig uit, dus
 * dit is de bedoeling opgeschreven, niet een tweede knop.
 */
html[data-theme] body[data-theme-backdrop="on"][data-depth="flat"] > .theme-backdrop-layer {
    background-image:
        linear-gradient(var(--bd-tint-fill), var(--bd-tint-fill)),
        var(--theme-backdrop, none);
    background-size: auto, var(--theme-backdrop-size, cover);
    background-position: 0 0, var(--theme-backdrop-position, 0 0);
}

html[data-theme] body[data-theme-backdrop="on"][data-depth="flat"] {
    background-color: var(--background-primary);
}

@media (prefers-reduced-transparency: reduce) {
    html[data-theme] body > .theme-backdrop-layer {
        display: none;
    }
}

/* ==== css/theme-character.css ==== */
/* Karakter — wat een theme mag zijn behalve een palet.

   Een theme leverde dertien platte kleuren, en dat is waarom 214 themes op
   één theme in 214 tinten lijken: twee themes konden verschillen in tint en
   verder in niets. Niet in vorm, niet in gewicht, niet in of hun vlakken
   doorzichtig zijn.

   renderThemeCSSBlock (handlers.go) schrijft nu acht tokens mee, allemaal uit
   getypte velden op ThemeColors en allemaal geklemd op de server -- er komt
   geen CSS uit een theme, alleen getallen en woorden uit een vaste lijst. Het
   nulwaarde van elk veld is precies het huidige gedrag, dus een theme dat
   niets over zijn karakter zegt ziet er nog uit zoals nu:

     --theme-surface-alpha    1      hoe massief een opgetild vlak is
     --theme-surface-blur     0px    de blur erachter
     --theme-surface-glow     0      hoeveel accent er omheen bloedt
     --theme-glow-lift        1      of dat een halo is (1) of een schaduw (0)
     --theme-radius-scale     1      vermenigvuldigt de radius-tokens
     --theme-label-transform  none   kapitalen op de categoriekop
     --theme-label-spacing    normal letterafstand daarvan
     --theme-label-weight     700    gewicht daarvan
*/

/* ── Vorm ────────────────────────────────────────────────────────────────
   Eén getal beweegt alle 352 hoeken in de app. De schaal wordt op de tokens
   zelf toegepast, niet op de plekken die ze gebruiken, dus er hoeft geen
   enkele view-CSS voor te veranderen. De pil blijft een pil: die is rond
   omdat hij rond hoort, niet omdat het theme dat vindt. */

/* Bij effecten uit staat de radiusschaal op 1 in plaats van op wat het theme
   vraagt: vorm is net zo goed een effect als glans, en "uit" dat de hoeken
   laat staan is niet uit. Geschreven als interpolatie tussen 1 en de schaal,
   want CSS kan niet op een waarde vertakken. */

html[data-theme] body {
    --theme-radius-applied: calc(1 + (var(--theme-radius-scale, 1) - 1) * var(--theme-effects, 1));

    --radius-1: calc(2px * var(--theme-radius-applied, 1));
    --radius-2: calc(3px * var(--theme-radius-applied, 1));
    --radius-3: calc(4px * var(--theme-radius-applied, 1));
    --radius-4: calc(6px * var(--theme-radius-applied, 1));
    --radius-5: calc(8px * var(--theme-radius-applied, 1));
    --radius-6: calc(10px * var(--theme-radius-applied, 1));
    --radius-7: calc(12px * var(--theme-radius-applied, 1));

    /*
     * De moderne laag draait mee.
     *
     * --layout-radius-* stond op vaste pixels in dashboard-enhancements.css,
     * dus de schaal van een theme bewoog de ene familie hoeken wel en de
     * andere niet. Dat viel niet op zolang geen enkel theme om een schaal
     * vroeg: --radius-5 en --layout-radius-sm waren allebei 8px. Sinds elk
     * theme een karakter heeft liepen ze uit elkaar -- het zoekveld in Health
     * bleef op 8px staan terwijl datzelfde veld in Config meebewoog.
     */
    --layout-radius-xs: calc(4px * var(--theme-radius-applied, 1));
    --layout-radius-sm: calc(8px * var(--theme-radius-applied, 1));
    --layout-radius-md: calc(12px * var(--theme-radius-applied, 1));
}

/* ── Letter ──────────────────────────────────────────────────────────────
   De categoriekop is het enige element dat een sectie benoemt, en daarmee de
   plek waar een terminal als terminal leest en een bibliotheek als
   bibliotheek. Het lettertype blijft van de lezer -- dat staat in
   Instellingen en een theme dat het stilletjes vervangt gaat te ver.

   --theme-title-spacing en --theme-title-transform bestonden al in
   theme-depth.css, met een handmatig bijgehouden lijst theme-ids erboven. Die
   lijst blijft staan en blijft winnen voor de acht themes die erin genoemd
   worden -- html[data-theme="x"] body is specifieker dan wat hier staat. Voor
   alle andere themes geldt vanaf nu wat het theme zelf zegt. Zodra die acht
   hun eigen velden invullen kan de lijst weg; tot die tijd verandert er voor
   ze niets, en dat is de bedoeling. */

html[data-theme] body {
    --theme-title-spacing: var(--theme-label-spacing, normal);
    --theme-title-transform: var(--theme-label-transform, none);
}

html[data-theme] body:not([data-depth="flat"]) .category-title {
    font-weight: var(--theme-label-weight, 700);
}

/* ── Glas ────────────────────────────────────────────────────────────────
   De vierde dieptestand. Flat, soft en rich verschillen in hoeveel het theme
   achter de inhoud getekend wordt; glas verschilt in of je er doorheen kijkt.

   Alleen hier worden de vlakken doorzichtig. Bij de andere drie blijven ze
   massief: wie om flat vroeg vroeg niet om doorkijk.

   De doorzichtigheid gaat op de tokens, dus elke plek die --background-modal
   of --layout-surface schildert doet automatisch mee. De blur kan dat niet --
   backdrop-filter is een eigenschap van een element, geen kleur - en staat
   daarom op de lijst hieronder. Die is niet verzonnen maar afgeleid: het zijn
   de selectors die die twee tokens vandaag daadwerkelijk schilderen. Zodra de
   .nd-* classlaag er is vervangt één naam de hele lijst. */

html[data-theme] body[data-depth="glass"] {
    --theme-tint: 5%;
    --theme-depth: 1.5;

    --layout-surface: color-mix(in srgb,
        var(--background-secondary) calc(var(--theme-surface-alpha, 1) * 78%), transparent);
    /* De modal leest mee op de overlaytrede in plaats van op de alpha van het
       theme: hij draagt tekst waar de lezer op handelt. */
    --background-modal: var(--surface-glass-overlay);
    --layout-modal-bg: var(--background-modal);

    /*
     * Drie tredes, geordend naar hoeveel er op een vlak gelezen wordt.
     *
     * Eén alpha voor alles was het probleem: dan is een contextmenu net zo
     * doorzichtig als de ondergrond, terwijl dat juist het vlak is waarvan de
     * lezer de tekst gebruikt om iets te doen. De eerste ontwerpronde kwam om
     * precies die reden onleesbaar terug.
     *
     * Alle drie schalen mee met wat het theme zelf vraagt -- surfaceAlpha komt
     * uit ThemeColors en staat op 1 tenzij een theme anders zegt -- behalve de
     * bodem onder de overlaytrede. Die is er omdat leesbaarheid geen
     * themakeuze is: wat tekst draagt om op te handelen staat op vaste grond,
     * en haalt zijn diepte uit zijn randen en zijn schaduw.
     */
    /* Chrome draagt nauwelijks tekst, dus die trede mag het verst open: 62%
       in plaats van 70%. De twee tredes eronder blijven waar ze staan, want
       daar wordt wel gelezen. */
    --surface-glass-page: color-mix(in srgb,
        var(--background-secondary) calc(var(--theme-surface-alpha, 1) * 62%), transparent);
    --surface-glass-slab: color-mix(in srgb,
        var(--background-secondary) calc(var(--theme-surface-alpha, 1) * 88%), transparent);
    /* Vast, niet meeschalend: een theme mag zijn blokken zo dun maken als het
       wil, maar niet het menu waar de lezer uit kiest. max() binnen color-mix
       parst bovendien niet -- de hele waarde valt dan weg. */
    --surface-glass-overlay: color-mix(in srgb,
        var(--background-secondary) 96%, transparent);

    /*
     * En de blur erachter, in dezelfde volgorde. Een overlay houdt er net
     * genoeg van over om zijn rand van de pagina te scheiden; meer zou over
     * zijn eigen tekst heen wassen.
     */
    /*
     * Licht valt de andere kant op als de pagina zelf licht is.
     *
     * Op een donker theme is wat doorschijnt donker, en dat mag iets opgetild:
     * dan leest het vlak als glas met licht erachter. Op een licht theme is het
     * wit, en optrekken wast de tekst weg -- daar hoort het juist iets omlaag.
     * --ink-dir zegt welke van de twee (1 donker, -1 licht), dus dit rekent het
     * uit in plaats van het per theme te herhalen: 1.06 tegen 0.98.
     */
    /*
     * Was 1.02 + 0.04 -- 1.06 on a dark theme -- and that is a lamp, not a
     * pane. Every surface at this depth lifted what was behind it by six per
     * cent, so a column of tiles read as a column of lighter boxes instead of
     * as the page seen through glass. What is left is a fifth of that: enough
     * that the pane is not nothing, little enough that you notice the colour
     * rather than the light.
     */
    --glass-brightness: calc(1.004 + 0.008 * var(--ink-dir, 1));

    /*
     * And the colour, which is the half that should do the work.
     *
     * Glass takes its character from what it lets through. Trading the
     * brightness for saturation is the whole of this change: the same backdrop
     * arrives with its hue intact instead of washed a step toward white.
     */
    --glass-saturate: 1.8;

    /*
     * The gloss, tinted and halved.
     *
     * A gloss theme sets `sheen` up to 1, and the base gloss paints white at
     * 12% and 15% -- times sheen, times this depth's 1.5, so on Gloss Obsidian
     * that is a 20% white band across the head of every surface. On a solid
     * surface that reads as lacquer. On a pane it reads as a lamp pointed at
     * the glass, and it is the single loudest thing in a column of tiles.
     *
     * Two changes, both in the same direction as the brightness above: the
     * light is cut to about a third, and it carries the theme's own colour
     * instead of being white. A reflection in tinted glass is tinted -- and
     * the colour is the part worth noticing.
     */
    --glass-gloss-light: color-mix(in srgb, var(--accent-primary) 45%, white);

    --surface-gloss:
        radial-gradient(120% 70% at 18% 0%,
            color-mix(in srgb, var(--glass-gloss-light) calc(4.5% * var(--theme-sheen, 0) * var(--theme-depth)), transparent) 0%,
            transparent 62%),
        linear-gradient(180deg,
            color-mix(in srgb, var(--glass-gloss-light) calc(5.5% * var(--theme-sheen, 0) * var(--theme-depth)), transparent) 0%,
            color-mix(in srgb, var(--glass-gloss-light) calc(1.5% * var(--theme-sheen, 0) * var(--theme-depth)), transparent) 42%,
            transparent 58%);

    /*
     * The lit edges, held back at this depth.
     *
     * They are written once for every depth as a percentage times
     * --theme-depth, and glass sets that to 1.5 -- so the 16% top edge lands
     * at 24% and every tile wears a bright rim. On a solid surface that rim is
     * the depth cue; on a pane it is a reflection, and a reflection that
     * strong on eight tiles in a column is what reads as "too present". Nine
     * and five, before the multiplier, so glass keeps an edge without a stripe.
     */
    --edge-top: inset 0 1px 0 color-mix(in srgb,
        var(--text-primary) calc((7% + 8% * var(--theme-sheen, 0)) * var(--theme-depth)), transparent);
    --edge-side: inset 1px 0 0 color-mix(in srgb,
        var(--text-primary) calc(5% * var(--theme-depth)), transparent);

    --glass-blur-page: calc(var(--theme-surface-blur, 0px) * 1.25);
    --glass-blur-slab: calc(var(--theme-surface-blur, 0px) * 0.6);
    --glass-blur-overlay: calc(var(--theme-surface-blur, 0px) * 0.25);

    /*
     * Glas hangt, het ligt niet.
     *
     * De slagschaduw is dezelfde als die van rich, en dat leest als plastic:
     * een doorzichtig vlak met een massieve schaduw eronder is geen glas maar
     * een gekleurd plaatje. Zestig procent van de val, zodat het vlak nog los
     * staat van de pagina zonder erop te drukken.
     */
    --surface-cast: 0 calc(1.2px * var(--theme-depth)) calc(6px * var(--theme-depth))
        color-mix(in srgb, black calc(14% * var(--theme-depth)), transparent);

    /*
     * En iets om doorheen te kijken.
     *
     * Dit is waar glas op valt of staat: blur van een egale pagina levert niets
     * op, dus zonder verloop achter de vlakken is doorzichtigheid alleen een
     * lichtere kleur. De twee wolken die elke diepte al tekent staan hier
     * sterker en krijgen een derde, laag in beeld, zodat een vlak halverwege de
     * pagina ook iets te vertonen heeft.
     */
    --depth-wash:
        radial-gradient(70rem 50rem at 10% -14%,
            color-mix(in srgb, var(--accent-primary) calc(13% * var(--theme-depth)), transparent),
            transparent 70%),
        radial-gradient(58rem 40rem at 98% 4%,
            color-mix(in srgb, var(--accent-warning) calc(9% * var(--theme-depth)), transparent),
            transparent 70%),
        radial-gradient(60rem 42rem at 40% 105%,
            color-mix(in srgb, var(--accent-success) calc(7% * var(--theme-depth)), transparent),
            transparent 72%);
}

/* ── De drie tredes, toegepast ──────────────────────────────────────────
   Dezelfde lijst als hiervoor, nu gesplitst naar hoeveel er op een vlak
   gelezen wordt. De chrome mag het meest doorkijken, blokken zitten
   ertussenin, en wat tekst draagt om op te handelen houdt net genoeg blur
   over om zijn rand van de pagina te scheiden. */

/* Chrome: knoppen, tabs, de balk. Weinig tekst, veel doorkijk. */
html[data-theme] body[data-depth="glass"] :is(
    .page-overview-header-btn,
    .page-navigation,
    .page-nav-btn[data-view-tab],
    /* De kop: de tabs, de chip en het actieblok zijn chrome zoals de knoppen
       hierboven. Zonder deze regels stond het actieblok wel op glas en de
       pagina's ernaast als kale omlijning -- twee materialen in een balk. */
    .header-track .page-nav-btn,
    .header-track .page-nav-overflow,
    .header-shortcuts,
    .header-destinations .library-link-anchor,
    .header-destinations .config-link-anchor,
    .button-container,
    .search-button,
    .btn-group,
    .tag-cloud-toggle-btn.search-button
) {
    backdrop-filter: blur(var(--glass-blur-page)) saturate(var(--glass-saturate, 1.4)) brightness(var(--glass-brightness, 1.02));
    -webkit-backdrop-filter: blur(var(--glass-blur-page)) saturate(var(--glass-saturate, 1.4)) brightness(var(--glass-brightness, 1.02));
}

/* Kale kopknoppen zijn kaal: zonder vlak is er geen glas om door te kijken,
   en de blur tekende toch een getinte doos achter elk icoon. Alleen "elk in
   een eigen doos" houdt het glas. */
html[data-theme] body[data-depth="glass"][data-header-buttons="plain"] .header-top :is(
    .page-overview-header-btn,
    .page-navigation,
    .page-nav-btn,
    .page-nav-overflow,
    .header-shortcuts,
    .dashboard-link-anchor,
    .library-link-anchor,
    .config-link-anchor,
    .search-button,
    .btn-group
) {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}

/* Ook onder "elk in een eigen doos" staan de acties en de paginatabs in de kop
   kaal, dus zonder glas. Een dock aan de rand is wel een plaat en houdt het;
   de segmented schaal is de plaat van de tabs en houdt het ook. */
html[data-theme] body[data-depth="glass"]:not([data-action-bar="bottom"]):not([data-action-bar="left"]):not([data-action-bar="right"]) .header-top :is(
    .header-shortcuts,
    .search-button,
    .btn-group
),
html[data-theme] body[data-depth="glass"] .header-top .header-track :is(.page-nav-btn, .page-nav-overflow),
html[data-theme] body[data-depth="glass"]:not([data-page-switcher="segmented"]) .header-top .page-navigation {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}

/* Blokken en rails: een lijst om te lezen, geen venster om door te kijken. */
html[data-theme] body[data-depth="glass"] :is(
    .config-tile,
    .config-panel,
    .config-choices,
    #dashboard-layout > .empty-state,
    .feed-row,
    .quickstart-card,
    .inbox-item,
    .lvs-summary,
    /* .lvs-group staat hier bewust niet: een rail is een lijst, geen plaat. Op
       200px breed en tien rijen hoog leest de slagschaduw als een harde rand
       met gloed langs beide zijden -- de rijen dragen hun eigen hover en hun
       eigen actieve staat. Config's rail draagt dezelfde klassen, dus beide
       zien er hetzelfde uit. */
    .lvs-filter,
    .health-monitor-stat
) {
    backdrop-filter: blur(var(--glass-blur-slab)) saturate(var(--glass-saturate, 1.4)) brightness(var(--glass-brightness, 1.02));
    -webkit-backdrop-filter: blur(var(--glass-blur-slab)) saturate(var(--glass-saturate, 1.4)) brightness(var(--glass-brightness, 1.02));
}

/* Overlays: bijna dekkend, en de blur alleen nog voor de randscheiding. */
html[data-theme] body[data-depth="glass"] :is(
    .modal,
    .search-container,
    .bookmark-form-modal-dialog,
    .health-focus-card
) {
    backdrop-filter: blur(var(--glass-blur-overlay)) saturate(var(--glass-saturate, 1.4)) brightness(var(--glass-brightness, 1.02));
    -webkit-backdrop-filter: blur(var(--glass-blur-overlay)) saturate(var(--glass-saturate, 1.4)) brightness(var(--glass-brightness, 1.02));
}

/*
 * En de klassieke layout, waar de meeste mensen zitten.
 *
 * Hierboven worden --layout-surface en --background-modal doorzichtig, en dat
 * dekt de moderne layout, de modals en het zoekvenster. In classic komen de
 * opgetilde vlakken ergens anders vandaan: theme-depth.css schildert de
 * kaart-presets rechtstreeks met --surface-1 en --surface-2. Het
 * widgetlichaam staat hieronder apart: het houdt de kleur van de pagina. Zonder deze regel bleef glas daar dus onzichtbaar, en dat is
 * precies de layout waar de default op staat.
 *
 * Het gaat op de achtergrond van die elementen en niet op --surface-2 zelf,
 * en dat is met opzet: theme-ink.css leidt de inkt af van --surface-2, en een
 * doorzichtige surface zou via oklch(from ...) een doorzichtige tekstkleur
 * opleveren. Het vlak mag doorzichtig zijn, de inkt die erop ligt niet.
 *
 * Wat glas hier níet doet is de kaart-presets die geen vlak hebben er een
 * geven. Masonry en de standaardweergave tekenen bewust geen kaart; daar
 * blijft glas dus beperkt tot de overlays. Doorzichtigheid is
 * een eigenschap van een vlak, en waar geen vlak is valt niets door te kijken.
 */
html[data-theme] body[data-depth="glass"] .dashboard-grid.layout-cards .category {
    background-color: color-mix(in srgb,
        var(--surface-2) calc(var(--theme-surface-alpha, 1) * 100%), transparent);
    backdrop-filter: blur(var(--theme-surface-blur, 0px)) saturate(var(--glass-saturate, 1.4)) brightness(var(--glass-brightness, 1.02));
    -webkit-backdrop-filter: blur(var(--theme-surface-blur, 0px)) saturate(var(--glass-saturate, 1.4)) brightness(var(--glass-brightness, 1.02));
}

html[data-theme] body[data-depth="glass"] .dashboard-grid.layout-widgets .category {
    background-color: color-mix(in srgb,
        var(--surface-1) calc(var(--theme-surface-alpha, 1) * 100%), transparent);
    backdrop-filter: blur(var(--theme-surface-blur, 0px)) saturate(var(--glass-saturate, 1.4)) brightness(var(--glass-brightness, 1.02));
    -webkit-backdrop-filter: blur(var(--theme-surface-blur, 0px)) saturate(var(--glass-saturate, 1.4)) brightness(var(--glass-brightness, 1.02));
}

/*
 * Kaartglas in een layout zonder kaarten.
 *
 * Standaard, Compact en Masonry tekenen geen vlak om een categorie, dus daar
 * had de kaartglas-schuif niets om op te werken. Wie eigen kaartglas kiest
 * (data-card-glass="own") krijgt er alsnog een ruit. Alleen dan: een glas-theme
 * dat iemand nooit heeft aangeraakt houdt de vlakke kolommen die het had.
 * Padding binnen de kolom, zodat de kolombreedte niet verandert.
 */
html[data-theme] body[data-depth="glass"][data-card-glass="own"] .dashboard-grid:is(.layout-default, .layout-compact, .layout-masonry) .category {
    background-color: color-mix(in srgb,
        var(--surface-2) calc(var(--theme-surface-alpha, 1) * 100%), transparent);
    backdrop-filter: blur(var(--theme-surface-blur, 0px)) saturate(var(--glass-saturate, 1.4)) brightness(var(--glass-brightness, 1.02));
    -webkit-backdrop-filter: blur(var(--theme-surface-blur, 0px)) saturate(var(--glass-saturate, 1.4)) brightness(var(--glass-brightness, 1.02));
    border-radius: var(--radius-6, 10px);
    padding: var(--space-3, 0.75rem);
}

html[data-theme] body[data-depth="glass"][data-card-glass="own"][data-card-border="on"] .dashboard-grid:is(.layout-default, .layout-compact, .layout-masonry) .category {
    outline: 1px solid color-mix(in srgb, var(--text-primary) 16%, transparent);
    outline-offset: -1px;
}

@media (prefers-reduced-transparency: reduce) {
    html[data-theme] body[data-depth="glass"][data-card-glass="own"] .dashboard-grid:is(.layout-default, .layout-compact, .layout-masonry) .category {
        background-color: var(--surface-2);
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }
}

/*
 * Het widgetlichaam onder glas.
 *
 * Zelfde vlak als buiten glas -- de kleur van de pagina, een stap ernaast --
 * maar doorschijnend en met blur, en met de lichte bovenrand die een ruit
 * leesbaar maakt als ruit. De cellen erin gaan mee: een massieve cel op een
 * doorzichtig vlak leest weer als een grijs blokje.
 */
html[data-theme] body[data-depth="glass"] .dashboard-widget .dashboard-widget-body {
    background-color: color-mix(in srgb,
        var(--dashboard-widget-surface) calc(var(--theme-surface-alpha, 1) * 100%), transparent);
    backdrop-filter: blur(var(--theme-surface-blur, 0px)) saturate(var(--glass-saturate, 1.4)) brightness(var(--glass-brightness, 1.02));
    -webkit-backdrop-filter: blur(var(--theme-surface-blur, 0px)) saturate(var(--glass-saturate, 1.4)) brightness(var(--glass-brightness, 1.02));
    box-shadow: var(--edge-light), var(--surface-cast);
}

html[data-theme] body[data-depth="glass"] .dashboard-widget {
    --dashboard-widget-cell: color-mix(in srgb,
        var(--dashboard-widget-cell-solid) calc(var(--theme-surface-alpha, 1) * 100%), transparent);
}

/*
 * De dunne rand om de ruiten, voor wie ze scherper wil zien tegen een drukke
 * achtergrond (Appearance → Oppervlak → Kaartglas). Een outline naar binnen en
 * geen border: een border verschuift de inhoud en komt op elke ruit anders
 * uit, en de box-shadow van het widgetlichaam is al bezet.
 */
html[data-theme] body[data-depth="glass"][data-card-border="on"] .dashboard-grid.layout-cards .category,
html[data-theme] body[data-depth="glass"][data-card-border="on"] .dashboard-grid.layout-widgets .category,
html[data-theme] body[data-depth="glass"][data-card-border="on"] .dashboard-widget .dashboard-widget-body {
    outline: 1px solid color-mix(in srgb, var(--text-primary) 16%, transparent);
    outline-offset: -1px;
}

@media (prefers-reduced-transparency: reduce) {
    html[data-theme] body[data-depth="glass"] .dashboard-grid.layout-cards .category,
    html[data-theme] body[data-depth="glass"] .dashboard-grid.layout-widgets .category {
        background-color: var(--surface-2);
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }

    html[data-theme] body[data-depth="glass"] .dashboard-widget .dashboard-widget-body {
        background-color: var(--dashboard-widget-surface);
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }

    html[data-theme] body[data-depth="glass"] .dashboard-widget {
        --dashboard-widget-cell: var(--dashboard-widget-cell-solid);
    }
}

/*
 * Eén vlak per widget.
 *
 * Een widget tekent een eigen kaart, uit de tijd dat een categorie er geen
 * had. Waar de categorie zelf al een ruit is -- Cards en Widgets, en bij eigen
 * kaartglas ook Default, Compact en Masonry -- stond die kaart in de kaart, en
 * het weerbericht drie lagen diep. Daar laat het widgetlichaam zijn vlak, rand,
 * schaduw en blur vallen en lijnt het uit met de kop, zoals de bookmarkrijen.
 * De cellen erin (weerrijen, stattegels, trendrijen) blijven de ene stap
 * boven de ruit. Zonder ruit om zich heen houdt de widget zijn kaart: dan is
 * hij het enige vlak tussen de inhoud en de backdrop.
 */
html[data-theme] body .dashboard-grid:is(.layout-cards, .layout-widgets) .dashboard-widget .dashboard-widget-body,
html[data-theme] body[data-depth="glass"][data-card-glass="own"] .dashboard-grid:is(.layout-default, .layout-compact, .layout-masonry) .dashboard-widget .dashboard-widget-body {
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
    box-shadow: none;
    outline: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}

/* ── Gloed ───────────────────────────────────────────────────────────────
   Nul is de vlakke rand die elk theme vandaag heeft, en blijft de default voor
   een theme dat om stilte vraagt.

   Niet aan de glas-stand gekoppeld. Gloed is iets van het theme, niet van de
   diepte: Retro CRT Mk II hoort te gloeien omdat fosfor gloeit, ook als je op
   rich staat. Alleen flat sluit hem uit, want daar heeft de lezer gezegd dat
   hij geen lagen wil.

   Op de schaduw-tokens en niet op de elementen. Dat was eerst anders en dat
   was fout: een eigen box-shadow-regel op dezelfde selectors vervangt de
   schaduw die er al staat in plaats van er iets aan toe te voegen -- box-shadow
   stapelt binnen één declaratie, niet over declaraties heen. Via de tokens
   blijft de bestaande schaduw de eerste laag en komt de gloed erachteraan.

   --surface-glow-ring is die tweede laag, één keer geschreven in plaats van
   vier keer uitgerekend. Bij gloed 0 is elke lengte 0 en de kleur volledig
   doorzichtig, dus een theme dat -1 declareert rendert precies zoals het
   vandaag rendert.

   De vier lengtes erin staan niet vast, want een glans heeft twee vormen. Op
   een donkere pagina staat licht óm het vlak; op een lichte pagina is dat een
   veeg, en leest alleen het accent dat ónder het vlak in de schaduw zit. De
   server zegt met --theme-glow-lift welke van de twee het is (1 is halo, 0 is
   schaduw), als getal en niet als woord: CSS kan geen kant op met wat een
   variabele zégt, maar met een getal kan het rekenen. Elke lengte staat dus
   tussen de twee standen in en wordt er door lift heen geschoven.

   De gloedsterkte zit alleen in de kleur en niet in de lengtes. Dat was eerst
   andersom en dat werkte niet: bij een zachte gloed van 0.12 kromp de hele
   schaduw mee tot 0,7px en was er niets meer te zien, precies bij de themes
   die deze laag moest bereiken. Nu verandert sterkte hoe sterk de gloed is en
   niet hoe groot -- en bij 0 is de kleur volledig doorzichtig, dus een theme
   dat -1 declareert schildert nog altijd niets. */

html[data-theme] body:not([data-depth="flat"]) {
    --glow-y: calc(6px + 4px * var(--theme-glow-lift, 1));
    --glow-blur: calc(18px + 8px * var(--theme-glow-lift, 1));
    --glow-spread: calc(-6px - 6px * var(--theme-glow-lift, 1));
    /* Dezelfde knop als de bloom in theme-depth.css: één sterkte voor beide
       gloeilagen, zodat "rustiger" niet op twee plekken apart hoeft. */
    --glow-pct: calc((34% + 16% * var(--theme-glow-lift, 1)) * var(--glow-strength, 0) * var(--theme-effects, 1));

    --surface-glow-ring:
        0 var(--glow-y) var(--glow-blur) var(--glow-spread)
        color-mix(in srgb, var(--accent-primary)
            calc(var(--theme-surface-glow, 0) * var(--glow-pct)), transparent);
}

/* Twee tokens, want de app heeft twee kaart-idiomen: --edge-light is de lichte
   bovenrand die theme-depth.css op de klassieke kaarten en widgets zet, en
   --layout-shadow-* zijn die van de moderne layout. */

html[data-theme] body:not([data-depth="flat"]) {
    --edge-light: var(--edge-top), var(--edge-side), var(--surface-glow-ring);
}

/* ── Gloed in classic ────────────────────────────────────────────────────
   De klassieke layout heeft nooit opgetilde vlakken gehad en krijgt ze hier
   ook niet: dit zet alleen de accentlaag, geen grijze schaduw eronder.

   De kaart-presets, het widgetlichaam, de modal en de theme-browser-tegels
   staan er niet bij, want die schilderen --edge-light al en gloeien dus mee
   vanaf de regel hierboven. Wat hier staat zijn de vlakken die in classic
   helemaal geen box-shadow hadden -- en dat is waarom de config view daar tot
   nu toe vlak bleef, welk theme je ook koos.

   De repeterende rijen (.feed-row, .inbox-item,
   .config-crud-row) staan er bewust niet bij. Vijftig gloeiende rijen onder
   elkaar is ruis en geen diepte, en in classic hebben die rijen geen vlak om
   op te tillen. In modern gloeien ze mee via --layout-shadow-*, zoals nu. */

html[data-theme] body:not([data-depth="flat"]) :is(
    .config-tile,
    .config-panel,
    .config-choices,
    .lvs-summary,
    .quickstart-card,
    .health-monitor-stat
) {
    /* De twee randen plus de schaduw, in één declaratie omdat box-shadow niet
       stapelt over regels heen. De gloed staat er niet apart bij: die zit sinds
       de hergroepering hierboven al in --edge-light zelf, en twee keer dezelfde
       ring schilderen maakt hem alleen maar harder. */
    box-shadow: var(--edge-light), var(--edge-dark), var(--surface-cast);
}

/*
 * Het commandovenster krijgt dezelfde twee randen, maar zijn eigen val.
 *
 * Het stond in de lijst hierboven en kreeg daarmee de cast van een plaat: 3px
 * omlaag, 15px zacht. Dat is de schaduw van iets dat op de pagina ligt, en dit
 * ligt erboven -- de overlay-spec tekent 0 24px 64px. De randen blijven, want
 * die zijn de taal van de app; alleen de diepte hoort bij wat het ding is.
 */
html[data-theme] body:not([data-depth="flat"]) .search-container {
    box-shadow:
        var(--edge-light),
        var(--edge-dark),
        0 24px 64px color-mix(in srgb, black calc(60% * var(--theme-depth)), transparent);
}

/* Doorzichtigheid is een van de dingen waar een lezer expliciet nee tegen kan
   zeggen. Dan valt glas terug op rich: de diepte blijft, de doorkijk gaat weg. */
@media (prefers-reduced-transparency: reduce) {
    html[data-theme] body[data-depth="glass"] {
        --layout-surface: var(--background-secondary);
        --background-modal: var(--background-primary);
    }
    html[data-theme] body[data-depth="glass"] :is(
        .config-tile,
    .config-panel,
    .config-choices,
    .page-overview-header-btn,
    .page-navigation,
    .page-nav-btn[data-view-tab],
    .search-button,
    .btn-group,
    .tag-cloud-toggle-btn.search-button,
    #dashboard-layout > .empty-state,
    .feed-row,
    .search-container,
    .button-container,
    .quickstart-card,
    .inbox-item,
    .lvs-summary,
    .lvs-filter,
    .health-monitor-stat,
    .modal,
    .health-focus-card
    ) {
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }
}



/* ==== css/category-header.css ==== */
/* De categoriekop.

   Vijf manieren om te tekenen wat er boven de bookmarks van een categorie
   staat, gekozen in Appearance → Weergave en gelezen van <body>:

     data-cat-head    theme | clean | underlined | boxed | label | group
     data-cat-size    s | m | l
     data-cat-icon    on | off
     data-cat-count   on | off
     data-cat-accent  on | off      (de lijn onder "underlined")

   "theme" doet niets: de kop blijft zoals het theme en de layout hem tekenen,
   en alles hieronder staat achter een andere waarde. Omdat de regels op <body>
   hangen is wisselen een herschildering en geen nieuwe render.

   De "//" voor de naam is ook de greep waarmee je een categorie versleept. Bij
   de andere stijlen verdwijnt het teken maar de greep niet: hij wordt een rijtje
   puntjes dat verschijnt als je de kop aanwijst. */

/* ---- grootte ---- */
body[data-cat-size="s"] { --cat-head-scale: 0.85; }
body[data-cat-size="l"] { --cat-head-scale: 1.2; }

body[data-cat-size="s"] .dashboard-grid .category-title,
body[data-cat-size="l"] .dashboard-grid .category-title {
    font-size: calc(var(--font-size-category) * var(--cat-head-scale, 1));
}

/* ---- icoon ---- */
body[data-cat-icon="off"] .dashboard-grid .category-title-icon,
body[data-cat-icon="off"] .dashboard-grid .category-title-label .bookmark-icon {
    display: none;
}

/* ---- aantal bookmarks, rechts van de naam ---- */
.category-title-count {
    display: none;
}

body[data-cat-count="on"] .dashboard-grid .category-title-count {
    display: inline;
    margin-left: 0.7em;
    font-size: 0.75em;
    font-weight: 400;
    letter-spacing: 0;
    text-transform: none;
    opacity: 0.65;
    font-variant-numeric: tabular-nums;
}

/* ---- alles behalve de eigen stijl van het theme ---- */
body:not([data-cat-head="theme"]) .dashboard-grid .category-reorder-handle {
    display: inline-block;
    /* rem, not em: against its own font-size of 0 an em is nothing, the box
       was zero wide and the icon after it took every click on the grip. */
    width: 0.9rem;
    font-size: 0;
    opacity: 1;
}

body:not([data-cat-head="theme"]) .dashboard-grid .category-reorder-handle::before {
    content: "\22EE\22EE";
    font-size: 0.8rem;
    letter-spacing: -0.25em;
    opacity: 0;
    transition: opacity 0.15s ease;
}

body:not([data-cat-head="theme"]) .dashboard-grid .category-title:hover .category-reorder-handle::before {
    opacity: 0.55;
}

/* The box is font-size 0 here, so an outline on it is a line: the grip
   shows focus instead. */
body:not([data-cat-head="theme"]) .dashboard-grid .category-reorder-handle:focus-visible {
    outline: none;
}

body:not([data-cat-head="theme"]) .dashboard-grid .category-reorder-handle:focus-visible::before {
    opacity: 1;
    background: var(--bookmark-row-selected-bg);
    color: var(--text-primary);
    border-radius: 4px;
}

body:not([data-cat-head="theme"]) .dashboard-grid .category-reorder-handle.is-static::before {
    content: none;
}

body:not([data-cat-head="theme"]) .dashboard-grid.layout-terminal .category-title-label::before {
    content: none;
}

/* ---- clean: alleen de naam ---- */
body[data-cat-head="clean"] .dashboard-grid .category-title {
    letter-spacing: 0;
    font-weight: 600;
}

/* ---- underlined: een lijn onder de naam ---- */
body[data-cat-head="underlined"] .dashboard-grid .category-title {
    padding-bottom: 0.4em;
    margin-bottom: 0.4em;
    letter-spacing: 0.02em;
    border-bottom: 1px solid color-mix(in srgb, var(--text-tertiary) 45%, transparent);
}

body[data-cat-head="underlined"][data-cat-accent="on"] .dashboard-grid .category-title {
    border-bottom: 2px solid var(--accent-primary);
}

/* ---- boxed: de naam op een eigen vlak ---- */
body[data-cat-head="boxed"] .dashboard-grid .category-title {
    padding: 0.4em 0.85em;
    margin-bottom: 0.5em;
    letter-spacing: 0.02em;
    font-size: calc(var(--font-size-category) * 0.9 * var(--cat-head-scale, 1));
    border-radius: calc(8px * var(--theme-radius-scale, 1));
    background: color-mix(in srgb, var(--background-secondary) calc(var(--theme-surface-alpha, 1) * 100%), transparent);
    backdrop-filter: blur(var(--theme-surface-blur, 0px));
    -webkit-backdrop-filter: blur(var(--theme-surface-blur, 0px));
}

/* ---- label: kleine hoofdletters, ruim gespatieerd ---- */
body[data-cat-head="label"] .dashboard-grid .category-title {
    font-size: calc(0.72rem * var(--cat-head-scale, 1));
    text-transform: uppercase;
    letter-spacing: 0.14em;
    font-weight: 700;
    color: var(--text-tertiary);
}

/* ---- group: de hele categorie op een vlak, de naam boven een lijn ---- */
body[data-cat-head="group"] .dashboard-grid .category {
    padding: 0.75em 0.85em 0.85em;
    border-radius: calc(10px * var(--theme-radius-scale, 1));
    background: color-mix(in srgb, var(--background-secondary) calc(var(--theme-surface-alpha, 1) * 55%), transparent);
    backdrop-filter: blur(var(--theme-surface-blur, 0px));
    -webkit-backdrop-filter: blur(var(--theme-surface-blur, 0px));
}

body[data-cat-head="group"] .dashboard-grid .category-title {
    padding-bottom: 0.5em;
    margin-bottom: 0.5em;
    letter-spacing: 0.02em;
    font-size: calc(var(--font-size-category) * 0.9 * var(--cat-head-scale, 1));
    border-bottom: 1px solid color-mix(in srgb, var(--text-tertiary) 25%, transparent);
}

@media (prefers-reduced-transparency: reduce) {
    body[data-cat-head="boxed"] .dashboard-grid .category-title,
    body[data-cat-head="group"] .dashboard-grid .category {
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }
}

/* ==== css/theme-browser.css ==== */
/* The theme browser.

   Built on the tokens theme-depth.css derives, so the modal is painted in the
   theme being browsed and every card sits on the same surface ladder as the
   dashboard behind it. */

.modal--theme-browser .modal-text {
    /* The grid brings its own spacing; the modal's prose padding would only
       narrow it. */
    text-align: left;
}

.theme-browser {
    display: flex;
    flex-direction: column;
    gap: var(--space-3, 0.75rem);
    max-height: min(70vh, 40rem);
}

.theme-browser-bar {
    display: flex;
    gap: var(--space-2, 0.6rem);
    align-items: center;
    flex-wrap: wrap;
}

.theme-browser-search {
    flex: 1 1 14rem;
    min-width: 0;
    padding: var(--space-2, 0.45rem) var(--space-2, 0.6rem);
    border-radius: var(--layout-radius-sm, 8px);
    border: 1px solid var(--hairline-strong, var(--border-primary));
    background: var(--surface-1, var(--background-secondary));
    color: var(--text-primary);
    font: inherit;
}

.theme-browser-search:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: 1px;
}

.theme-browser-segments {
    display: inline-flex;
    gap: var(--space-1, 0.25rem);
}

/*
 * Under the search: All, Favourites, Light and Dark, the ones reached for
 * most, and "More filters" at the end for character and collection. Folded,
 * those two take no room; one that is on shows as a chip with a × below.
 */
.theme-browser-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1, 0.25rem) var(--space-2, 0.5rem);
}

.theme-browser-more {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: 0.35em;
    padding: var(--space-1, 0.3rem) var(--space-2, 0.6rem);
    border: 0;
    border-radius: var(--layout-radius-sm, 8px);
    background: transparent;
    color: var(--text-secondary);
    font: inherit;
    font-size: 0.85em;
    cursor: pointer;
}

.theme-browser-more::after {
    content: "▸";
    font-size: 0.85em;
}

.theme-browser-more[aria-expanded="true"]::after {
    content: "▾";
}

.theme-browser-more:hover {
    color: var(--text-primary);
}

.theme-browser-more-count {
    display: inline-grid;
    place-items: center;
    min-width: 1.4em;
    height: 1.4em;
    padding: 0 0.3em;
    border-radius: 999px;
    background: var(--accent-primary);
    color: var(--background-primary);
    font-size: 0.85em;
}

.theme-browser-active {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1, 0.25rem);
}

.theme-browser-chip--clear span {
    color: var(--text-secondary);
}

.theme-browser-segment,
.theme-browser-variant {
    padding: var(--space-1, 0.3rem) var(--space-2, 0.6rem);
    border-radius: var(--layout-radius-pill, 999px);
    border: 1px solid var(--hairline, var(--border-secondary));
    background: transparent;
    color: var(--text-secondary);
    font: inherit;
    font-size: 0.85em;
    cursor: pointer;
}

.theme-browser-segment.is-on,
.theme-browser-variant.is-on {
    background: var(--accent-wash, color-mix(in srgb, var(--accent-primary) 16%, transparent));
    border-color: var(--accent-primary);
    color: var(--text-primary);
}

/*
 * De archetyperij.
 *
 * Twaalf chips passen niet naast het zoekveld en de vier segmenten, dus ze
 * staan op een eigen regel en mogen aflopen. Wrap in plaats van scroll: een
 * rij die horizontaal schuift verbergt precies wat je zoekt als je niet weet
 * dat er meer is.
 */

.theme-browser-characters {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1, 0.25rem);
    margin-bottom: var(--space-2, 0.5rem);
}

.theme-browser-chip {
    padding: var(--space-1, 0.25rem) var(--space-2, 0.55rem);
    border-radius: var(--layout-radius-pill, 999px);
    border: 1px solid var(--hairline, var(--border-secondary));
    background: transparent;
    color: var(--text-tertiary);
    font: inherit;
    font-size: 0.8em;
    cursor: pointer;
}

.theme-browser-chip:hover {
    color: var(--text-secondary);
}

.theme-browser-chip.is-on {
    background: var(--accent-wash, color-mix(in srgb, var(--accent-primary) 16%, transparent));
    border-color: var(--accent-primary);
    color: var(--text-primary);
}

/* De geschreven regel staat boven de afgeleide eigenschappen: hij is de zin
   en die zijn de etiketten. Niet afgekapt: de kaart is de enige plek waar hij
   staat, en een halve zin is geen zin. De regels zijn op lengte gehouden waar
   ze geschreven zijn (theme_description.go), dus dit kost hooguit een derde
   regel op de smalste kaart. */

.theme-browser-card-line {
    margin: 0;
    color: var(--text-secondary);
    font-size: 0.85em;
    line-height: 1.35;
}

/* How many cards the filters leave, at the end of the In use line. */
.theme-browser-count {
    flex: none;
    color: var(--text-tertiary);
    white-space: nowrap;
}

/* The theme in use, kept above the grid however far it scrolls. */
.theme-browser-inuse {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    padding: var(--space-1, 0.35rem) var(--space-2, 0.6rem);
    border: 1px solid color-mix(in srgb, var(--accent-primary) 45%, transparent);
    border-radius: var(--layout-radius-sm, 8px);
    background: color-mix(in srgb, var(--accent-primary) 8%, transparent);
    font-size: 0.85em;
}

.theme-browser-inuse-text {
    flex: 1;
    min-width: 0;
}

.theme-browser-inuse.is-chosen {
    border-style: dashed;
}

.theme-browser-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
    gap: var(--space-2, 0.6rem);
    overflow-y: auto;
    /* Room for the focus ring, which would otherwise be clipped by the scroll
       container on the first and last row. */
    padding: 2px;
}

.theme-browser-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-1-5, 0.4rem);
    padding: var(--space-2, 0.55rem);
    border-radius: var(--layout-radius-md, 12px);
    border: 1px solid var(--hairline, var(--border-secondary));
    background: var(--surface-1, var(--background-secondary));
    box-shadow: var(--edge-light, none);
    cursor: pointer;
    transition: border-color var(--layout-transition, 0.2s ease),
                transform var(--layout-transition, 0.2s ease);
}

.theme-browser-card:hover,
.theme-browser-card:focus-visible {
    border-color: var(--accent-primary);
    transform: translateY(-1px);
    outline: none;
}

.theme-browser-card.is-current {
    border-color: var(--accent-primary);
    box-shadow: var(--edge-light, none), 0 0 0 1px var(--accent-primary) inset;
}

/* The swatch bar is the whole point of a grid over a list: at this many themes
   nobody chooses by name. Six stops, in the order the eye needs them — the two
   surfaces first, then the text that has to sit on them, then the accents. */
.theme-browser-swatches {
    display: flex;
    height: 1.6rem;
    border-radius: var(--layout-radius-sm, 8px);
    overflow: hidden;
    border: 1px solid var(--hairline, var(--border-secondary));
}

.theme-browser-swatch {
    flex: 1 1 0;
}

.theme-browser-card-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-1-5, 0.4rem);
}

.theme-browser-card-name {
    font-weight: 600;
    color: var(--text-primary);
    overflow-wrap: anywhere;
}

.theme-browser-star {
    border: none;
    background: none;
    padding: 0 var(--space-hairline, 0.1rem);
    cursor: pointer;
    color: var(--text-tertiary);
    font-size: 1em;
    line-height: 1;
}

.theme-browser-star.is-on {
    color: var(--accent-warning);
}

.theme-browser-card-traits {
    margin: 0;
    color: var(--text-tertiary);
    font-size: 0.8em;
}

.theme-browser-card-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-1-5, 0.4rem);
    margin-top: auto;
}

.theme-browser-variants {
    display: inline-flex;
    gap: var(--space-1, 0.2rem);
}

.theme-browser-single,
.theme-browser-current {
    color: var(--text-tertiary);
    font-size: 0.78em;
}

.theme-browser-current {
    color: var(--accent-primary);
}

.theme-browser-empty {
    margin: var(--space-4, 1rem) 0;
    color: var(--text-tertiary);
    grid-column: 1 / -1;
}

@media (max-width: 40rem) {
    .theme-browser-grid {
        grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
    }
    .theme-browser {
        max-height: 75vh;
    }
}

/* The picker and its Browse button, side by side.
   The picker keeps the width it had; the button takes only what it needs, so
   the field still reads as one control with a way out of it. */
.config-theme-row {
    display: flex;
    gap: var(--space-2, 0.5rem);
    align-items: stretch;
}

.config-theme-row .config-theme-picker {
    flex: 1 1 auto;
    min-width: 0;
}

.config-theme-browse {
    flex: 0 0 auto;
    white-space: nowrap;
}

/* Gloss themes: a badge beside the name, and the light they catch drawn over
   the swatches, so the card shows the difference before it is picked. */
.theme-browser-badge {
    flex: 0 0 auto;
    padding: 1px 7px;
    border-radius: 999px;
    border: 1px solid color-mix(in srgb, var(--accent-primary) 55%, transparent);
    background: linear-gradient(180deg,
        color-mix(in srgb, white 22%, var(--accent-wash, transparent)) 0%,
        var(--accent-wash, transparent) 60%);
    color: var(--accent-primary);
    font-size: 0.72em;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

/* The latest collection: filled rather than outlined, so it reads apart from
   the archetype badge beside it. */
.theme-browser-badge--new {
    border-color: var(--accent-primary);
    background: var(--accent-primary);
    color: var(--background-primary);
}

/* A theme of the reader's own: filled in the info colour, so it stands apart
   from NEW. LOOK and RECOLOURED stay outlined, as notes beside it. */
.theme-browser-badge--yours {
    border-color: var(--accent-info, var(--accent-primary));
    background: var(--accent-info, var(--accent-primary));
    color: var(--background-primary);
}

.theme-browser-badge--look,
.theme-browser-badge--recoloured {
    border-color: color-mix(in srgb, var(--text-secondary) 55%, transparent);
    background: transparent;
    color: var(--text-secondary);
}

/* ✎ Edit / Recolour on a card: quiet until the card is pointed at. */
.theme-browser-edit {
    margin-left: auto;
    padding: 1px 7px;
    border: 1px solid var(--border-secondary, var(--border-primary));
    border-radius: 6px;
    background: transparent;
    color: var(--text-secondary);
    font: inherit;
    font-size: 0.78em;
    cursor: pointer;
}

.theme-browser-card:not(:hover):not(:focus-within):not(.is-current) .theme-browser-edit {
    opacity: 0.55;
}

.theme-browser-edit:hover,
.theme-browser-edit:focus-visible {
    border-color: var(--accent-primary);
    color: var(--text-primary);
    opacity: 1;
}

/* The editor on the Themes tab, and the way into it from the other tabs. */
.look-studio-back {
    margin-bottom: var(--space-2, 0.6rem);
}

.look-studio-edit-link {
    margin: 0 0 var(--space-2, 0.6rem);
}

.config-theme-look-parts {
    margin: 0 0 8px;
    color: var(--text-primary);
}

/* "Use this theme's look": under the In use line, quiet until needed. */
.theme-browser-look-switch {
    display: inline-flex;
    align-items: center;
    gap: 0.45em;
    color: var(--text-secondary);
    font-size: 0.85em;
    cursor: pointer;
}

/* "Your themes" and "Built in", across the whole grid row. */
.theme-browser-group-head {
    grid-column: 1 / -1;
    margin: var(--space-2, 0.6rem) 0 0;
    color: var(--text-tertiary);
    font-size: 0.78em;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.theme-browser-group-head:first-child {
    margin-top: 0;
}

.theme-browser-card-name {
    margin-right: auto;
}

/* ── The look studio ──────────────────────────────────────────────────
   The browser as a panel docked on the right rather than a modal in the
   middle: what is being chosen is how the page looks, so most of the page
   stays in view. It lies over the dashboard and the page keeps its layout:
   squeezing the columns to make room changed the very thing being judged.
   Below 768px it is a sheet over the lower part of the screen instead.

   Placed with top/left and a width rather than inset or right: 0, which on a
   page with a classic scrollbar lands a gutter short of the edge.

   490px so the six tabs fit on one line; at 440 the Layout tab scrolled out
   of sight. */

:root {
    --look-studio-width: min(490px, 100vw);
}

/* The bottom-left cards sit above everything and are inert while the studio
   is open; on a phone they covered the sheet. They come back on close. */
body.look-studio-open .quickstart-card {
    display: none;
}

/* The dialog an ℹ in the panel opens: over the panel, which is added to the
   page later at the same z-index. */
body.look-studio-open #app-modal {
    z-index: 2001;
}

.look-studio {
    position: fixed;
    top: 0;
    left: calc(100vw - min(var(--look-studio-width), 100vw));
    width: min(var(--look-studio-width), 100vw);
    height: 100vh;
    height: 100dvh;
    z-index: 2000;
    display: flex;
    flex-direction: column;
    min-height: 0;
    box-sizing: border-box;
    background: var(--surface-1, var(--background-secondary));
    color: var(--text-primary);
    border-left: 1px solid var(--hairline-strong, var(--border-primary));
    box-shadow: -8px 0 28px rgba(0, 0, 0, 0.18);
}

/* The left edge, dragged to widen the panel. Over the border, a little to
   either side of it, so it is easy to catch. */
.look-studio-resize {
    position: absolute;
    top: 0;
    bottom: 0;
    left: -4px;
    width: 8px;
    z-index: 1;
    cursor: ew-resize;
    touch-action: none;
}

.look-studio-resize::after {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: 3px;
    width: 2px;
    background: var(--accent-primary, var(--text-primary));
    opacity: 0;
    transition: opacity 0.15s;
}

.look-studio-resize:hover::after,
.look-studio-resize:focus-visible::after,
.look-studio.is-resizing .look-studio-resize::after {
    opacity: 0.6;
}

.look-studio-resize:focus-visible {
    outline: none;
}

.look-studio.is-resizing {
    user-select: none;
}

.look-studio-head {
    padding: var(--space-3, 0.75rem) var(--space-3, 0.9rem) 0;
}

.look-studio-title {
    display: flex;
    align-items: baseline;
    gap: var(--space-2, 0.5rem);
}

.look-studio-title h2 {
    margin: 0;
    font-size: 1rem;
}

/* The title takes the free space, so the × stays in the corner also where the
   key line is hidden (the sheet on a phone). */
.look-studio-title h2 {
    margin-right: auto;
}

.look-studio-title .look-studio-keys {
    margin-left: 0;
}

/* The ×, drawn like the bookmark form's. */
.look-studio-close {
    flex-shrink: 0;
    align-self: center;
    width: 28px;
    height: 28px;
    border: none;
    border-radius: var(--radius-5, 8px);
    background: transparent;
    color: inherit;
    font: inherit;
    font-size: 1.3rem;
    line-height: 1;
    cursor: pointer;
}

.look-studio-close:hover,
.look-studio-close:focus-visible {
    background: var(--background-secondary, rgba(128, 128, 128, 0.12));
}

.look-studio-keys {
    margin-left: auto;
    color: var(--text-tertiary);
    font-size: 0.75em;
}

.look-studio-tabs {
    display: flex;
    gap: 2px;
    margin-top: var(--space-2, 0.6rem);
    border-bottom: 1px solid var(--hairline-strong, var(--border-primary));
    overflow-x: auto;
}

.look-studio-tab {
    position: relative;
    flex: 0 0 auto;
    padding: 0.45rem 0.6rem;
    border: 0;
    border-bottom: 2px solid transparent;
    background: none;
    color: var(--text-secondary);
    font: inherit;
    font-size: 0.85em;
    cursor: pointer;
}

.look-studio-tab.is-on {
    color: var(--text-primary);
    border-bottom-color: var(--accent-primary);
}

.look-studio-tab:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: -2px;
}

/* The dot on a tab whose answers differ from when the studio opened. */
/* In the tab's corner rather than beside its name: six tabs have no width
   to spare for six dots that are mostly hidden. */
.look-studio-dot {
    position: absolute;
    top: 0.3rem;
    right: 0.15rem;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--accent-warning, #fbbf24);
    visibility: hidden;
}

.look-studio-tab.is-dirty .look-studio-dot {
    visibility: visible;
}

.look-studio-pane {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    /* A touch scroll that reaches the end stops there instead of moving the
       page; theme-browser.js does the same for the wheel. */
    overscroll-behavior: contain;
    padding: var(--space-3, 0.75rem) var(--space-3, 0.9rem);
}

.look-studio-pane:focus {
    outline: none;
}

/* The theme grid scrolls with the pane rather than inside it. */
.look-studio .theme-browser {
    max-height: none;
}

.look-studio .theme-browser-grid {
    grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
    overflow: visible;
}

/* Appearance's panels, without the page's spacing around them. */
.look-studio-pane .config-panel {
    margin: 0 0 var(--space-3, 0.75rem);
}

.look-studio-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    padding: var(--space-2, 0.6rem) var(--space-3, 0.9rem);
    border-top: 1px solid var(--hairline-strong, var(--border-primary));
}

.look-studio-scope {
    display: flex;
    flex: 1 1 auto;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    font-size: 0.85em;
}

/*
 * The second footer row: every button on one line. A translation too long
 * for that wraps, and Cancel and Apply then move down together, at the end,
 * rather than one of them being cut off or left on a row of its own.
 */
.look-studio-row {
    display: flex;
    flex-wrap: wrap;
    flex-basis: 100%;
    align-items: center;
    gap: 0.4rem;
}

.look-studio-row .look-studio-btn {
    padding-inline: 0.5rem;
}

.look-studio-actions {
    display: flex;
    gap: 0.4rem;
    margin-left: auto;
}

.look-studio-btn--quiet {
    font-size: 0.85em;
}

.look-studio-scope.is-off {
    opacity: 0.4;
}

.look-studio-seg button:disabled {
    cursor: default;
}

.look-studio-seg {
    display: inline-flex;
    border: 1px solid var(--hairline-strong, var(--border-primary));
    border-radius: var(--layout-radius-sm, 8px);
    overflow: hidden;
}

.look-studio-seg button {
    padding: 0.3rem 0.65rem;
    border: 0;
    background: none;
    color: var(--text-secondary);
    font: inherit;
    cursor: pointer;
}

.look-studio-seg button[aria-pressed="true"] {
    background: var(--accent-wash, color-mix(in srgb, var(--accent-primary) 18%, transparent));
    color: var(--accent-primary);
}

.look-studio-btn {
    padding: 0.35rem 0.75rem;
    border-radius: var(--layout-radius-sm, 8px);
    border: 1px solid var(--hairline-strong, var(--border-primary));
    background: var(--surface-2, var(--background-primary));
    color: var(--text-primary);
    font: inherit;
    font-size: 0.85em;
    cursor: pointer;
}

.look-studio-btn:disabled {
    opacity: 0.45;
    cursor: default;
}

/* A dialog over the panel itself: Save as theme… */
.look-studio-dialog {
    position: absolute;
    inset: 0;
    z-index: 5;
    display: grid;
    place-items: center;
    background: rgba(0, 0, 0, 0.45);
}

.look-studio-dialog-box {
    width: min(380px, calc(100% - 32px));
    padding: var(--space-4, 1rem);
    border: 1px solid var(--border-primary);
    border-radius: 12px;
    background: var(--background-modal, var(--background-secondary));
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
}

.look-studio-dialog-box h3 {
    margin: 0 0 var(--space-3, 0.75rem);
    font-size: 1em;
}

.look-studio-dialog-box .config-text {
    width: 100%;
    box-sizing: border-box;
}

.look-studio-check {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    margin: var(--space-3, 0.75rem) 0 0;
    cursor: pointer;
}

.look-studio-check small {
    display: block;
    margin-top: 2px;
    color: var(--text-secondary);
}

.look-studio-dialog-actions {
    display: flex;
    justify-content: flex-end;
    gap: 6px;
    margin-top: var(--space-4, 1rem);
}

.look-studio-btn--primary {
    border-color: transparent;
    background: var(--accent-primary);
    color: var(--background-primary);
    font-weight: 700;
}

.look-studio-btn:focus-visible,
.look-studio-seg button:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: 1px;
}

.look-studio-btn[data-studio-compare][aria-pressed="true"] {
    background: var(--accent-warning, #fbbf24);
    color: #000;
}

/* While Compare is held the panel steps back, so the page is what you see. */
.look-studio.is-comparing .look-studio-pane {
    opacity: 0.35;
}

.look-studio-looks {
    display: grid;
    gap: var(--space-2, 0.5rem);
    margin-top: var(--space-2, 0.5rem);
}

.look-studio-look {
    display: grid;
    grid-template-columns: 64px 1fr auto;
    align-items: center;
    gap: var(--space-3, 0.75rem);
    padding: var(--space-2, 0.5rem);
    border: 1px solid var(--hairline-strong, var(--border-primary));
    border-radius: var(--layout-radius-md, 10px);
}

.look-studio-look-sw {
    height: 44px;
    border-radius: var(--layout-radius-sm, 6px);
    background-color: var(--background-primary);
    --bd-scale: 0.5;
    --bd-strength: 1;
}

.look-studio-look-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    font-size: 0.85em;
}

.look-studio-look-text span {
    color: var(--text-tertiary);
}

/* What the look sets, smaller and quieter than what it is for. */
.look-studio-look-text .look-studio-look-parts {
    margin-top: 2px;
    font-size: 0.9em;
    opacity: 0.8;
}

.look-studio-looks-intro p {
    margin: 0 0 var(--space-2, 0.5rem);
    color: var(--text-secondary);
    font-size: 0.85em;
    line-height: 1.45;
}

@media (max-width: 767px) {
    .look-studio {
        top: 45vh;
        left: 0;
        width: 100vw;
        height: 55vh;
        border-left: 0;
        border-top: 1px solid var(--hairline-strong, var(--border-primary));
        border-radius: 14px 14px 0 0;
        box-shadow: 0 -8px 28px rgba(0, 0, 0, 0.2);
    }

    .look-studio-resize {
        display: none;
    }

    .look-studio-keys {
        display: none;
    }
}

/* ==== css/stat-tile.css ==== */
/*
 * A figure with a caption under it.
 *
 * The app draws this in two places and drew it as two components: config's
 * statistics (`.config-tile`, sixteen call sites across three template
 * helpers) and a dashboard widget's readout (`.dashboard-widget-stat`, built
 * as DOM by `statGrid`). They had already drifted twice -- the label case and
 * tracking, then the delta tones -- and each drift was found by eye and fixed
 * by hand in two files. One component ends that: what a figure looks like is
 * decided here, once.
 *
 * Both markup sites keep their own class as well, because both carry rules
 * that genuinely belong to their setting: config's tile has the severity
 * stripe down its left edge, and a widget cell paints the ground that draws
 * the hairline grid around it. What moved here is everything that was the
 * same idea written twice.
 *
 * Two sizes, and they should stay different. A statistics tile is the content
 * of its page; a widget figure is one cell in a tile on a dashboard. The size
 * is the only thing that varies -- treatment, tone and rhythm are shared.
 */

.stat-tile {
    /*
     * The four measurements a size sets. Declared on the component rather than
     * hard-coded per variant, so a third size -- a compact statistics tile, a
     * figure in an overlay -- is four values and no new rules.
     */
    --stat-tile-gap: 3px;
    --stat-tile-pad-y: var(--space-2, 0.45rem);
    --stat-tile-pad-x: var(--space-2, 0.55rem);
    --stat-tile-value-size: 1.5em;
    --stat-tile-label-size: var(--font-size-small);

    position: relative;
    display: flex;
    flex-direction: column;
    gap: var(--stat-tile-gap);
    min-width: 0;
    padding: var(--stat-tile-pad-y) var(--stat-tile-pad-x);
    text-align: left;
    /* Buttons do not inherit either of these, and about half of these tiles
       are buttons -- a figure you can open. */
    font: inherit;
    color: inherit;
}

/* The page's own size: config's statistics, where the figure is the content. */
.stat-tile--md {
    --stat-tile-gap: 8px;
    --stat-tile-pad-y: 16px;
    --stat-tile-pad-x: 16px;
    --stat-tile-value-size: calc(var(--font-size-text) * 1.943);
    --stat-tile-label-size: calc(var(--font-size-text) * 0.823);
}

/*
 * The figure and its delta sit on one baseline.
 *
 * A row of their own rather than the delta nested inside the value: the value
 * clips with an ellipsis so a long one -- a size, a version, a date -- cannot
 * push the cell beside it out of the grid, and anything nested inside it gets
 * clipped along with it. Config nested it and got away with it only because a
 * statistics tile is wide enough never to overflow.
 */
.stat-tile-figure {
    display: flex;
    align-items: baseline;
    min-width: 0;
}

/*
 * The figure, and the whole point of a tile.
 *
 * Large, tight and lined up: a dashboard is scanned rather than read, so the
 * number has to carry at a glance and the word under it only has to survive a
 * second look. Tabular figures because these stack in columns, and a column of
 * proportional digits does not line up.
 */
.stat-tile-value {
    font-size: var(--stat-tile-value-size);
    font-weight: var(--font-weight-semibold, 600);
    line-height: 1.05;
    letter-spacing: -0.015em;
    font-variant-numeric: tabular-nums;
    min-width: 0;
    /* Long values -- a size, a version, a date -- shrink rather than push the
       cell beside them out of the grid. */
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/*
 * The caption, in the quiet treatment.
 *
 * Lower case and tracked out a little: a short dim word under a large number
 * needs the width to read as a caption rather than as a smudge. Config set
 * these in upper case at 0.06em for a long time, which made one idea read as
 * two objects; the widget's treatment won because the app lower-cases
 * everywhere it names something, from the category heading to the view title.
 */
.stat-tile-label {
    font-size: var(--stat-tile-label-size);
    letter-spacing: 0.03em;
    text-transform: none;
    color: var(--text-tertiary);
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* A sentence under the figure, where there is room for one. */
.stat-tile-detail {
    margin: 0;
    font-size: calc(var(--font-size-text) * 0.891);
    color: var(--text-muted, var(--text-tertiary));
}

/*
 * Tone by what the figure means, never by how large it is.
 *
 * Nought lost is the best news a tile can carry and still belongs in the lost
 * cell, so the tone says which figure this is rather than how alarming it is.
 */
.stat-tile--bad .stat-tile-value { color: var(--accent-error); }
.stat-tile--warn .stat-tile-value { color: var(--accent-warning); }
.stat-tile--good .stat-tile-value { color: var(--accent-success); }

/*
 * A nought is not an alarm.
 *
 * Tone says which figure this is; it should not also shout when the figure is
 * the good one. Every value used to be painted its row's colour, so a tile
 * reading "0 lost, 0 died, 4 broken" was three reds and the reader had to
 * check all three to find the one that wanted them. The good tone included:
 * green is the screen's word for "this is fine", and no healthy links is not
 * fine.
 */
.stat-tile.is-quiet .stat-tile-value {
    color: var(--text-tertiary);
    font-weight: var(--font-weight-normal, 400);
}

/*
 * Which way it moved, beside the figure that moved.
 *
 * Two modes and one set of rules. A delta on a *state* carries direction: one
 * more broken bookmark is worse, so it is red. A delta on a *count* does not:
 * one fewer bookmark this week is smaller, not worse, and colouring it red
 * would be the app having an opinion nobody asked for.
 */
.stat-tile-delta {
    flex: 0 0 auto;
    margin-left: var(--space-1, 0.25rem);
    font-size: var(--text-2xs, 0.65rem);
    font-weight: var(--font-weight-semibold, 600);
    font-variant-numeric: tabular-nums;
    color: var(--text-tertiary);
}

.stat-tile-delta--good { color: var(--accent-success); }
.stat-tile-delta--warn { color: var(--accent-warning); }
.stat-tile-delta--bad { color: var(--accent-error); }
.stat-tile-delta--neutral { color: var(--text-tertiary); }

/* "this week" after the number: the qualifier is what makes a delta mean
   anything, and it should not read as loud as the figure it qualifies. */
.stat-tile-delta-period {
    margin-left: 0.25em;
    font-weight: var(--font-weight-normal, 400);
    opacity: 0.75;
}

/*
 * The key that gets you there, on the figures that go somewhere.
 *
 * Every other affordance in the app prints its key in its label; a figure has
 * no room for that, so the chip sits in the corner and waits until the reader
 * is on the tile. A grid of figures each shouting its key would be louder than
 * the figures.
 */
.stat-tile-key {
    position: absolute;
    top: 2px;
    right: 3px;
    padding: 0;
    border: none;
    background: none;
    font-family: inherit;
    font-size: var(--text-2xs, 0.65rem);
    line-height: 1;
    color: var(--text-tertiary);
    opacity: 0;
    transition: opacity 0.16s ease;
}

.stat-tile:is(:hover, :focus-visible) .stat-tile-key {
    opacity: 0.75;
}

body.no-animations .stat-tile-key {
    transition: none;
}

button.stat-tile {
    border: 0;
    cursor: pointer;
}

/* ==== css/dashboard.css ==== */
/* Dashboard Styles */
* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

/* Remove tap highlight on mobile devices */
button,
a,
input,
select,
textarea,
[role="button"],
.clickable {
    -webkit-tap-highlight-color: transparent;
    -webkit-touch-callout: none;
}

/*
 * Reserve the scrollbar's width whether or not the page scrolls. Without this,
 * a short section (Pages & tags, a filtered list) has no scrollbar and a tall
 * one does, so switching between them changed the usable width and every
 * centred layout — config's nav column most visibly — jumped sideways. Safari
 * and Firefox on macOS reserve real width for a scrollbar; browsers with
 * overlay scrollbars never showed the shift, which is why it reads as a
 * platform bug rather than a layout one.
 */
html {
    scrollbar-gutter: stable;
}

body {
    min-height: 100vh;
    transition: background-color 0.3s ease, color 0.3s ease;
    font-size: var(--font-size-text);
    /* clip, not hidden: `overflow-x: hidden` promotes the other axis from
       visible to auto, which made body its own scroll container (computed
       "hidden auto") instead of letting the viewport scroll the page. That is
       why an overlay's body-level scroll lock could strand the page and why the
       scrollbar visibly belonged to body. clip suppresses horizontal overflow
       without creating a scroll container. layout-modern.css already uses it. */
    overflow-x: clip;
    width: 100%;
}

/* Dashboard Sections */
.dashboard-section {
    padding: 0;
}

.dashboard-section.section-controls {
    /*
     * The bar starts at the top of the window.
     *
     * 2rem of air above it was there to lift a card off the page; the bar runs
     * edge to edge now and carries no card, so the air was only pushing the
     * grid down. What is left is enough for the row not to touch the window's
     * own edge.
     */
    padding-top: var(--space-2, 0.5rem);
    padding-bottom: 0;
}

.dashboard-section.section-content {
    padding-top: 0;
    padding-bottom: 8rem;
}

.container {
    /* Fixed clamp() side margin (not a percentage) so the whitespace shrinks
       toward the floor as the window narrows, giving the columns the reclaimed
       space before they have to get narrower. Roomy margins keep the columns
       closer together on wide screens.

       The clamp is the "balanced" value of the side-margin setting, so a
       dashboard that never touches the setting looks exactly as it always has;
       the fallback here repeats it for the case where no body attribute is set
       at all (a view rendered before setupDOM runs). */
    max-width: 1600px;
    width: 100%;
    margin: 0 auto;
    padding: 0 var(--dashboard-side-margin, clamp(2rem, 6vw, 8rem));
}

/* Left/right band beside the grid.
 *
 * Each keeps a clamp so the margin still collapses on a narrow window rather
 * than squeezing the columns — only the ceiling differs. "snug" hands roughly
 * 100px per side back to the grid on a wide screen; "airy" is for people who
 * want the columns pulled together. */
body[data-side-margin="snug"] {
    --dashboard-side-margin: clamp(var(--space-4, 1rem), 2.5vw, var(--space-7, 3rem));
}
body[data-side-margin="balanced"] {
    --dashboard-side-margin: clamp(var(--space-6, 2rem), 6vw, 8rem);
}
body[data-side-margin="airy"] {
    --dashboard-side-margin: clamp(var(--space-7, 3rem), 9vw, 12rem);
}

/* Header Top - Contains date, tabs, and config button */
.header-top {
    display: flex;
    justify-content: space-between;
    /* The identity block is two lines tall and the actions are one, so they
       align on the top rather than on a centre neither of them shares. */
    align-items: flex-start;
    gap: var(--space-4, 1rem);
}

/*
 * Identity: the view name, and the clock under it.
 *
 * These were a 32px strip of clock and buttons with a 60px band under it that
 * held nothing but the name, so the grid began 92px down the page to say one
 * word. One block now: the name owns the left edge and the clock is the quiet
 * line beneath it.
 */
.header-identity {
    display: flex;
    flex-direction: column;
    gap: var(--space-hairline, 0.15rem);
    min-width: 0;
}

/*
 * The clock line does not stretch.
 *
 * dashboard-enhancements.css gives .header-top-primary `flex: 1 1 12rem`,
 * which was right while its parent was the header row -- it meant "take the
 * width left over". Its parent is this column now, so the same declaration
 * means "take the height left over", and one line of clock grew to 192px.
 */
.header-identity > .header-top-primary {
    flex: 0 0 auto;
}

.date {
    display: flex;
    flex-direction: column;
    gap: var(--space-1, 0.25rem);
    font-size: calc(var(--font-size-controls) * 1.2);
    font-family: var(--font-family-main);
    font-weight: var(--font-weight-semibold);
    letter-spacing: 0;
    text-transform: none;
    line-height: 1.2;
    color: var(--text-secondary);
}

.date-badge-mobile {
    display: none;
}

.date-time-line,
.date-weather-line {
    font-weight: var(--font-weight-semibold);
}

.date-weather-line--error {
    color: var(--text-secondary);
    font-size: calc(var(--font-size-controls) * 0.95);
    font-weight: var(--font-weight-normal, 400);
    opacity: 0.85;
}

.date-time-line {
    font-size: calc(var(--font-size-controls) * 1.3); /* slightly smaller */
    color: var(--text-secondary);
    cursor: pointer;
    user-select: none;
    border-radius: var(--layout-radius-sm, 8px);
    transition: opacity 0.12s ease;
}
.date-time-line:hover {
    opacity: 0.75;
}

/* ─── Date popover ───────────────────────────────────────────── */
.date-popover {
    position: fixed;
    z-index: 9000;
    background: var(--background-secondary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-7, 12px);
    padding: var(--space-3, 0.85rem) var(--space-4, 1rem) var(--space-3, 0.7rem);
    min-width: 210px;
    color: var(--text-primary);
    font-family: var(--font-family-main);
    box-shadow: 0 4px 18px color-mix(in srgb, var(--text-primary) 14%, transparent);
    animation: datePopoverIn 0.15s ease forwards;
}

@keyframes datePopoverIn {
    from { opacity: 0; transform: translateY(-6px) scale(0.97); }
    to   { opacity: 1; transform: translateY(0)   scale(1);    }
}

.date-popover-week-label {
    font-size: var(--text-2xs, 0.65rem);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-secondary);
    margin-bottom: var(--space-2, 0.55rem);
}

.date-popover-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 2px;
    text-align: center;
}

.date-popover-col-label {
    font-size: var(--text-2xs, 0.62rem);
    color: var(--text-tertiary);
    padding-bottom: var(--space-1, 0.25rem);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.date-popover-day {
    font-size: var(--text-sm, 0.78rem);
    font-weight: var(--font-weight-semibold, 600);
    padding: var(--space-1, 0.3rem) 0;
    border-radius: var(--radius-4, 6px);
    line-height: 1;
    color: var(--text-primary);
}

.date-popover-day.is-weekend {
    color: var(--text-tertiary);
}

.date-popover-day.is-today {
    background: color-mix(in srgb, var(--accent-primary) 22%, transparent);
    color: var(--text-primary);
}

.date-popover-footer {
    margin-top: var(--space-3, 0.7rem);
    padding-top: var(--space-2, 0.55rem);
    border-top: 1px solid var(--border-primary);
    text-align: center;
}

.date-popover-cal-link {
    font-size: var(--text-2xs, 0.68rem);
    color: var(--text-secondary);
    text-decoration: none;
    transition: color 0.1s ease;
}
.date-popover-cal-link:hover {
    color: var(--accent-primary);
    text-decoration: underline;
}

/* ─── Move popover ───────────────────────────────────────────── */
/* One sentence about one control, only while it is asked for.
   Fixed rather than absolute: the bookmark form scrolls its own body, and an
   absolutely-placed bubble would be clipped by it. Above .bookmark-form-modal
   (10050) and its confirm dialogs, since it can be opened from either. */
.field-popover {
    position: fixed;
    z-index: 10160;
    max-width: min(19rem, calc(100vw - 1rem));
    padding: var(--space-1-5, 0.4rem) var(--space-2, 0.6rem);
    border-radius: var(--radius-5, 0.5rem);
    border: 1px solid var(--border-primary);
    background: var(--background-secondary);
    color: var(--text-primary);
    font-family: var(--font-family-main);
    font-size: calc(var(--font-size-controls) * 0.85);
    line-height: 1.4;
    box-shadow: 0 6px 20px color-mix(in srgb, var(--text-primary) 16%, transparent);
    pointer-events: none;
}

.field-popover[hidden] {
    display: none;
}

/* A warning is not a description: same bubble, the colour of the field's own
   error state, so a shortcut clash cannot be mistaken for a hint. */
.field-popover.field-popover-warning {
    border-color: var(--accent-error);
    color: var(--accent-error);
}

.move-popover {
    position: fixed;
    z-index: 9000;
    background: var(--background-secondary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-5, 8px);
    padding: var(--space-1-5, 0.35rem) 0 var(--space-2, 0.45rem);
    min-width: 12rem;
    max-width: min(16rem, calc(100vw - 1rem));
    max-height: min(70vh, 20rem);
    overflow-y: auto;
    color: var(--text-primary);
    font-family: var(--font-family-main);
    /*
     * A drop, not a halo.
     *
     * This was 0 4px 18px of --text-primary at 14%, which on a dark theme is a
     * pale glow under the menu rather than a shadow -- the one surface in the
     * app lit from below. Black, and deeper, so the menu reads as standing over
     * the page the way every other floating surface does. Shallower than a
     * modal's 24/64: a menu is a hand's breadth off the page, not a sheet over
     * it.
     */
    box-shadow: 0 12px 32px color-mix(in srgb, black 45%, transparent);
    animation: datePopoverIn 0.13s ease forwards;
}

/*
 * The menus join the depth ladder.
 *
 * .move-popover is the one surface every menu in the app wears -- the bookmark
 * and category menus, config's own, the check-mode menu, and the move, tag and
 * delete pickers -- and it was drawing the same flat box at rich, glass and
 * flat alike. The edges and the cast come from the theme now, so the menus are
 * lit by whatever the reader chose, like the panels and the cards.
 *
 * No backdrop-filter, at any depth. Blur on a menu is what gave Safari a
 * composited layer that hit-tested in front of what it covered and swallowed
 * the clicks aimed at it; .health-view-menu lost its blur for exactly that and
 * these must not gain one. Edges and a drop cost no layer.
 */
html[data-theme] body:not([data-depth="flat"]) .move-popover {
    box-shadow:
        var(--edge-light),
        var(--edge-dark),
        0 12px 32px color-mix(in srgb, black calc(45% * var(--theme-depth)), transparent);
}

/* The category menu sizes to what it holds.
 *
 * .move-popover is capped at 16rem for the move, tag and delete pickers, which
 * list every category, page or tag you have and want to stay narrow. This menu
 * is four fixed rows, and each carries the key that does the same thing — at
 * the shared cap the widest row needed 274px and got 237, so the chip was
 * quietly cut off by the ellipsis. Longer translations make that worse: the
 * French label for spreading is half again as long as the English one. */
#category-context-menu {
    width: max-content;
    max-width: min(24rem, calc(100vw - 1rem));
}

/* The bookmark context menu is a fixed, short action list — unlike the move,
   tag and delete popovers, which share .move-popover and list every category,
   page or tag you have and genuinely need to scroll.
   The shared 20rem cap was already within one item of this menu's height, so
   adding the two multi-select entries pushed it over: the last entries fell
   below the fold and a scrollbar appeared on a menu that is meant to be read at
   a glance. It sizes to its content instead, and only falls back to scrolling
   when the viewport itself is too short to hold it. */
.bookmark-context-menu {
    max-height: none;
    overflow-y: visible;
    /* Its rows carry their key too, and at the shared 16rem cap the widest
       ("Checking (Periodic)…" with Shift+C) cut the chip to "Shi…". Sized to
       its content like the category menu above. */
    width: max-content;
    max-width: min(24rem, calc(100vw - 1rem));
}

@media (max-height: 34rem) {
    .bookmark-context-menu {
        max-height: calc(100vh - 2rem);
        overflow-y: auto;
    }
}

.move-popover-header {
    font-size: calc(var(--font-size-controls) * 0.72);
    font-weight: var(--font-weight-semibold);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-secondary);
    padding: var(--space-1-5, 0.4rem) var(--space-3, 0.85rem) var(--space-1, 0.25rem);
}

.move-popover-current-hint {
    padding: 0 var(--space-3, 0.85rem) var(--space-2, 0.55rem);
    font-size: calc(var(--font-size-controls) * 0.88);
    font-weight: var(--font-weight-medium);
    color: var(--text-primary);
    line-height: 1.35;
}

.move-popover-section-label {
    font-size: calc(var(--font-size-controls) * 0.68);
    font-weight: var(--font-weight-medium);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-tertiary);
    padding: var(--space-2, 0.5rem) var(--space-3, 0.85rem) var(--space-1, 0.2rem);
}

.move-popover-item {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    padding: var(--space-2, 0.45rem) var(--space-3, 0.85rem);
    font-size: calc(var(--font-size-controls) * 0.92);
    font-weight: var(--font-weight-medium);
    color: var(--text-primary);
    cursor: pointer;
    border-radius: var(--radius-4, 0.4rem);
    margin: 0 var(--space-1, 0.3rem);
    transition:
        background-color 0.12s ease,
        color 0.12s ease;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.move-popover-item.is-focused {
    background: color-mix(in srgb, var(--accent-primary) 12%, var(--background-primary));
    color: var(--text-primary);
}

.move-popover-item.is-current {
    color: var(--text-primary);
    background: color-mix(in srgb, var(--accent-primary) 8%, var(--background-primary));
    cursor: default;
}

.move-popover-item.is-current.is-focused {
    background: color-mix(in srgb, var(--accent-primary) 12%, var(--background-primary));
}

.move-popover-item.is-danger {
    color: var(--accent-error);
}

.move-popover-item.is-danger.is-focused {
    background: color-mix(in srgb, var(--accent-error) 14%, var(--background-primary));
    color: var(--accent-error);
}

.move-popover-item.is-danger .move-popover-check {
    color: var(--accent-error);
}

.move-popover-check {
    width: 0.9rem;
    flex-shrink: 0;
    font-size: var(--text-xs, 0.75rem);
    color: var(--accent-primary);
    text-align: center;
}

.move-popover-divider {
    height: 1px;
    margin: var(--space-1-5, 0.35rem) var(--space-2, 0.5rem);
    background: var(--border-primary);
}

/* Marks an entry that opens a submenu rather than acting immediately. */
.move-popover-submenu-caret {
    margin-left: auto;
    padding-left: var(--space-2, 0.5rem);
    font-size: var(--text-xs, 0.7rem);
    color: var(--text-tertiary);
}

/* The current value of an entry that opens a submenu — "3 columns" beside
   "Width", so the menu answers as well as asks. It takes over the trailing
   alignment from whatever follows it, or the row would grow three separate
   right-aligned blocks. */
.move-popover-item-detail {
    margin-left: auto;
    padding-left: var(--space-2, 0.5rem);
    font-size: var(--text-xs, 0.7rem);
    color: var(--text-tertiary);
    white-space: nowrap;
}

.move-popover-item-detail ~ .move-popover-submenu-caret,
.move-popover-item-detail ~ .move-popover-item-key {
    margin-left: 0;
}

/* An entry that is present but cannot act — it still says what it is and, in
   its detail, why it cannot. Removing it would leave the user looking for a
   feature they read about. */
.move-popover-item.is-disabled {
    opacity: 0.55;
    cursor: default;
}

/* ─── Check-mode submenu ─────────────────────────────────────
   Three named modes, each with the sentence that distinguishes it — the same
   copy the health view shows, so the choice reads identically wherever it is
   made. Two-line rows need the alignment and wrapping the flat menu forbids. */
.bookmark-check-mode-menu {
    min-width: 17rem;
    max-width: min(20rem, calc(100vw - 1rem));
}

.bookmark-check-mode-menu .move-popover-item {
    align-items: flex-start;
    white-space: normal;
    overflow: visible;
}

.bookmark-check-mode-menu .move-popover-check {
    margin-top: var(--space-hairline, 0.15rem);
}

.check-mode-option-text {
    display: flex;
    flex-direction: column;
    gap: var(--space-hairline, 0.1rem);
    min-width: 0;
}

.check-mode-option-label {
    display: flex;
    align-items: center;
    gap: var(--space-1-5, 0.4rem);
}

/* The accelerator is printed on the option rather than taught elsewhere, so the
   keyboard route is discoverable from the menu itself. */
/* Keyboard hint on a menu row: pushed to the trailing edge so the labels stay
   left-aligned and the keys line up as a column of their own. Same chip as
   .check-mode-option-key, which sits inside a label rather than beside one. */
.move-popover-item-key {
    margin-left: auto;
    padding: 0 var(--space-1, 0.25rem);
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-2, 0.2rem);
    font-family: inherit;
    font-size: var(--text-2xs, 0.65rem);
    line-height: 1.4;
    color: var(--text-tertiary);
    white-space: nowrap;
}

.check-mode-option-key {
    padding: 0 var(--space-1, 0.25rem);
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-2, 0.2rem);
    font-family: inherit;
    font-size: var(--text-2xs, 0.65rem);
    line-height: 1.4;
    color: var(--text-tertiary);
}

.check-mode-option-body {
    font-size: var(--text-xs, 0.72rem);
    font-weight: var(--font-weight-normal);
    color: var(--text-secondary);
    white-space: normal;
}

/* On hover the row tints, so the explanation follows rather than staying grey
   against a coloured background. */
.bookmark-check-mode-menu .move-popover-item.is-focused .check-mode-option-body {
    color: inherit;
}

/* ─── Tag popover (Shift+T) ─────────────────────────────────── */
.tag-popover {
    min-width: 13rem;
    max-width: min(18rem, calc(100vw - 1rem));
    outline: none;
}

.tag-popover:focus {
    outline: none;
}

.tag-popover .move-popover-item {
    outline: none;
}

.tag-popover-bookmark-name {
    padding-bottom: var(--space-1-5, 0.35rem);
}

.tag-popover-current-tags {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1-5, 0.35rem) var(--space-1-5, 0.4rem);
    padding: 0 var(--space-3, 0.85rem) var(--space-3, 0.65rem);
}

.tag-popover-current-tags-label {
    font-size: calc(var(--font-size-controls) * 0.72);
    font-weight: var(--font-weight-medium);
    color: var(--text-secondary);
    margin-right: var(--space-hairline, 0.15rem);
}

.tag-popover-current-tags-none {
    font-size: calc(var(--font-size-controls) * 0.88);
    color: var(--text-tertiary);
    font-style: italic;
}

.tag-popover-current-chip {
    font-size: calc(var(--font-size-controls) * 0.78);
    padding: var(--space-hairline, 0.12rem) var(--space-2, 0.45rem);
}

.tag-popover-empty-hint {
    padding: var(--space-hairline, 0.15rem) var(--space-3, 0.85rem) var(--space-3, 0.65rem);
    font-size: calc(var(--font-size-controls) * 0.86);
    color: var(--text-secondary);
    line-height: 1.35;
}

.tag-popover-item-label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

.tag-popover-item-meta {
    flex: 0 0 auto;
    font-size: calc(var(--font-size-controls) * 0.72);
    color: var(--text-tertiary);
    margin-left: auto;
    padding-left: var(--space-1-5, 0.35rem);
}

.move-popover-item.is-current .tag-popover-item-meta {
    color: var(--text-secondary);
}

.date-weather-line {
    font-size: calc(var(--font-size-controls) * 1.6); /* larger for weather */
    color: var(--text-tertiary);
}

/* Add spacing below the date/weather block to separate from page title */
.dashboard-section.section-controls {
    padding-bottom: var(--space-5, 1.5rem);
}

.weather-icon {
    display: inline-flex;
    align-items: center;
    margin: 0 var(--space-1, 0.22rem) 0 var(--space-hairline, 0.18rem);
    vertical-align: -0.12em;
}

.weather-icon-svg {
    width: 1.05em;
    height: 1.05em;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.weather-text {
    vertical-align: baseline;
}

.title {
    margin: 0;
    line-height: 1.05;
    font-size: var(--font-size-title);
    font-family: var(--font-family-main);
    font-weight: var(--font-weight-black);
    opacity: 0.8;
    user-select: none;
    color: var(--text-primary);
}

/* Section trail under the view name. Sits in the header's own type scale, not
   the title's, so it reads as orientation rather than a second heading. */
.title-breadcrumb {
    margin: var(--space-hairline, 0.15rem) 0 0;
    font-family: var(--font-family-main);
    font-size: var(--font-size-controls);
    color: var(--text-secondary);
    user-select: none;
}

.title-breadcrumb[hidden] {
    display: none;
}

.dashboard-section.section-controls .header-actions {
    justify-self: end;
}

.header-actions {
    --header-control-gap: var(--space-2, 0.5rem);
    display: flex;
    align-items: center;
    gap: var(--header-control-gap);
    margin-left: auto;
    /* One line: the track inside it shrinks rather than the row breaking. */
    flex-wrap: nowrap;
    min-width: 0;
    /*
     * Takes the room the identity block does not.
     *
     * Content-sized, it stayed at its own intrinsic width while the header had
     * 750px going spare, and the seven action buttons that moved in here
     * overflowed it by 120px -- the config icon was pushed off the screen with
     * free space beside it. It grows to fill instead, and the track inside it
     * is what gives when there is genuinely not enough.
     */
    flex: 1 1 auto;
    justify-content: flex-end;
}

/*
 * The actions, as one control rather than a bar floating over the grid.
 *
 * They sat in .button-container, a slab pinned to a corner of the viewport,
 * which meant the things you do here lived somewhere else from the places you
 * go. One surround, one row, beside the destinations they are not: a hairline
 * between the two groups says which is which.
 *
 * Which of them are drawn is still the reader's, through the same
 * data-show-* attributes -- and the keys keep working whatever is shown,
 * because the handlers are on the document and never asked.
 */
.header-shortcuts {
    display: flex;
    align-items: center;
    /* Tighter than the gap between destinations: these are one control with
       several buttons in it, and the surround is what says so. */
    gap: var(--space-1, 0.25rem);
    flex: 0 0 auto;
    min-width: 0;
    padding: 0.15rem 0.25rem;
    border: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
    border-radius: var(--layout-radius-sm, 8px);
    background: transparent;
}

/*
 * An empty surround is a box around nothing.
 *
 * Each action is the reader's to switch off, and all three off leaves a border
 * with no buttons in it -- so the surround goes when the last one does.
 */
body[data-show-add-bookmark-button="false"][data-show-search-button="false"][data-show-commands-button="false"][data-show-finders-button="false"][data-show-tag-cloud-button="false"][data-show-recent-button="false"][data-show-pages-button="false"][data-show-collapse-all-button="false"][data-show-cheatsheet-button="false"] .header-shortcuts {
    display: none;
}

/*
 * A switched-off action stays switched off.
 *
 * The rules above that hide each button (body[data-show-*-button="false"] ...)
 * are one class and one attribute; the header treatment below is three
 * classes, so styling the buttons made them outrank the settings and a button
 * the reader had turned off came back. Repeated here at the header's own
 * weight, so the answer to "is it drawn" stays the reader's.
 */
body[data-show-add-bookmark-button="false"] .header-shortcuts #quick-add-toolbar-btn,
body[data-show-search-button="false"] .header-shortcuts #search-button,
body[data-show-commands-button="false"] .header-shortcuts #commands-button,
body[data-show-finders-button="false"] .header-shortcuts #finders-button,
body[data-show-recent-button="false"] .header-shortcuts .recent-button,
body[data-show-pages-button="false"] .header-shortcuts #page-overview-header-btn,
body[data-show-collapse-all-button="false"] .header-shortcuts #collapse-all-button,
body[data-show-cheatsheet-button="false"] .header-shortcuts .help-button {
    display: none;
}

/*
 * An action reads as a header control, not as a dock button.
 *
 * They arrived wearing what they wore while floating over the grid -- a filled
 * plate at 74% over nothing, a label beside every icon, a wider box -- which
 * beside the flat destination buttons read as two different kinds of thing in
 * one bar. They are the same kind of thing: something you press in the header.
 * So they take the destinations' treatment exactly (see .health-link--icon and
 * friends above): transparent, secondary ink, the same padding and corner, and
 * the same 2px underline that says which one you are on.
 */
.header-actions .header-shortcuts .search-button {
    display: inline-flex;
    align-items: center;
    flex-wrap: nowrap;
    gap: var(--space-1, 0.3rem);
    /* Inside a surround of its own, so tighter than a destination standing on
       the bare header: the padding that reads as room around a lone icon reads
       as a gap inside a box. */
    padding: var(--space-1, 0.25rem) var(--space-1-5, 0.4rem);
    border-radius: var(--radius-3, 4px);
    border: none;
    background: transparent;
    box-shadow: none;
    backdrop-filter: none;
    color: var(--text-secondary);
    font-family: var(--font-family-main);
    font-size: var(--font-size-controls);
    font-weight: var(--font-weight-semibold);
    min-width: 0;
    width: auto;
    height: auto;
    opacity: 1;
    transition: color 0.3s ease;
}

/* Inside the surround a button can take a fill on hover without competing
   with the bar: the box already bounds it. */
.header-actions .header-shortcuts .search-button:hover {
    color: var(--text-primary);
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
    text-shadow: none;
    transform: none;
}

.header-actions .header-shortcuts .search-button:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
}

/*
 * The word, then the key that does the same thing.
 *
 * A row of bare glyphs asks the reader to already know what + and > and ! do,
 * which is the thing a header is worst at teaching. Each action says what it
 * is and then shows the key, so the key is learned from the button rather than
 * from the manual -- the order the cheat sheet itself uses.
 */
.header-actions .header-shortcuts .search-button-label {
    display: inline;
    white-space: nowrap;
    text-transform: lowercase;
}

/*
 * The key reads as a key: quieter than the word it follows, in the same
 * tabular face the cheat sheet and the overlay foots use for a keycap.
 */
.header-actions .header-shortcuts .search-button-icon {
    font-family: var(--font-family-main);
    font-size: var(--font-size-controls);
    line-height: 1;
    color: var(--text-tertiary);
}

.header-actions .header-shortcuts .search-button:hover .search-button-icon {
    color: var(--text-secondary);
}

/*
 * One order, whatever the markup says.
 *
 * The buttons arrive in two groups left over from the dock, and which of them
 * are drawn is a setting -- so their order cannot come from the DOM or it
 * changes as buttons are switched on and off. Add first, because it is the one
 * that makes something, then search, then the list of what you opened last,
 * then the sheet that says what every key does. Commands, finders and fold-all
 * are not here: their keys still work from anywhere, and the row is shorter
 * for it.
 */
.header-actions .header-shortcuts #quick-add-toolbar-btn   { order: 1; }
.header-actions .header-shortcuts #search-button           { order: 2; }
.header-actions .header-shortcuts #commands-button         { order: 3; }
.header-actions .header-shortcuts #finders-button          { order: 4; }
.header-actions .header-shortcuts #tag-cloud-toggle-btn    { order: 5; }
.header-actions .header-shortcuts #recent-bookmarks-button { order: 6; }
.header-actions .header-shortcuts #page-overview-header-btn { order: 7; }
.header-actions .header-shortcuts #collapse-all-button     { order: 8; }
.header-actions .header-shortcuts #help-button             { order: 9; }

/*
 * The groups dissolve, so every button is a sibling of every other.
 *
 * `order` only sorts within one flex container, and the two groups are the
 * dock's own split -- primary and secondary, which meant something when they
 * were stacked in a corner and means nothing in a row. display: contents makes
 * the seven buttons direct children of .header-shortcuts, which is what lets
 * the order above hold however many of them are switched on.
 */
.header-actions .header-shortcuts .btn-group {
    display: contents;
}

/* A group with nothing left in it must not leave its gap behind. */
/* The destinations keep their width; the track is what gives. */
.header-actions > :not(.page-navigation) {
    flex: 0 0 auto;
}

/* inbox · health · config — same gap as between page tabs and the pages icon. */
.header-destinations {
    display: flex;
    align-items: center;
    gap: var(--header-control-gap, 0.5rem);
    min-width: 0;
    flex-shrink: 0;
}

.bookmark-link.find-hidden {
    display: none;
}

/* Tag filter view — same dashboard-grid / packed-columns as normal categories */
#dashboard-layout.dashboard-grid.tag-filter-view {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    grid-template-columns: unset;
    row-gap: var(--space-3, 0.75rem);
    width: 100%;
    max-width: 100%;
}

.tag-filter-banner {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-2, 0.55rem);
    padding: var(--space-1-5, 0.35rem) var(--space-2, 0.5rem) var(--space-3, 0.65rem);
    flex-shrink: 0;
    width: 100%;
}

.tag-filter-banner-head {
    width: 100%;
}

.tag-filter-bulk-toolbar {
    width: 100%;
}

#dashboard-layout.tag-filter-view .tag-filter-bulk-toolbar {
    margin-bottom: 0;
    padding-bottom: 0;
    border-bottom: none;
}

#dashboard-layout.tag-filter-view .tag-filter-bulk-toolbar .recent-bookmarks-open-btn.modal-button {
    flex: 0 1 auto;
    min-width: min(100%, 10rem);
}

.tag-filter-indicator-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1, 0.28rem);
    max-width: 100%;
    min-width: 0;
    padding: var(--space-1, 0.22rem) var(--space-2, 0.55rem);
    border: 1px solid color-mix(in srgb, var(--accent-primary) 55%, var(--border-primary));
    border-radius: var(--radius-pill, 999px);
    background: color-mix(in srgb, var(--accent-primary) 12%, var(--background-secondary));
    color: var(--text-primary);
    font-family: var(--font-family-main);
    font-size: calc(var(--font-size-controls) * 0.92);
    font-weight: var(--font-weight-semibold);
    line-height: 1.25;
    box-shadow: 0 1px 4px color-mix(in srgb, var(--accent-primary) 18%, transparent);
}

.tag-filter-indicator-prefix {
    color: var(--accent-primary);
    font-weight: var(--font-weight-bold);
}

.tag-filter-indicator-tag {
    color: var(--accent-primary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tag-filter-indicator-count {
    color: var(--text-secondary);
    font-weight: var(--font-weight-normal);
    font-size: calc(var(--font-size-controls) * 0.85);
    white-space: nowrap;
}

.tag-filter-indicator-count::before {
    content: '·';
    margin-right: var(--space-1, 0.28rem);
    opacity: 0.55;
}

/* Shown only while a tag filter is on — updateTagFilterIndicator sets `hidden`
   the rest of the time, and [hidden] would otherwise lose to this rule. */
.tag-filter-indicator {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1-5, 0.35rem);
    min-width: 0;
    max-width: 100%;
}

.tag-filter-indicator[hidden] {
    display: none;
}

.tag-filter-indicator-head {
    display: inline-flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-1-5, 0.35rem);
    max-width: 100%;
    min-width: 0;
}

.tag-filter-indicator-chips {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1, 0.3rem);
    min-width: 0;
    max-width: 100%;
}

.tag-filter-indicator-tag-item {
    display: inline-flex;
    align-items: center;
    gap: var(--space-hairline, 0.1rem);
    max-width: 100%;
    min-width: 0;
}

.tag-filter-indicator-tag-remove {
    flex-shrink: 0;
    border: 1px solid transparent;
    background: transparent;
    color: var(--text-tertiary);
    width: 1.25rem;
    height: 1.25rem;
    border-radius: var(--radius-pill, 999px);
    font-size: var(--text-base, 0.95rem);
    line-height: 1;
    cursor: pointer;
    transition:
        border-color 0.15s ease,
        color 0.15s ease,
        background-color 0.15s ease;
}

.tag-filter-indicator-tag-remove:hover,
.tag-filter-indicator-tag-remove:focus-visible {
    border-color: var(--accent-primary);
    color: var(--accent-primary);
    background: color-mix(in srgb, var(--background-primary) 94%, transparent);
    outline: none;
}

.tag-filter-indicator-summary {
    color: var(--text-secondary);
    font-family: var(--font-family-main);
    font-size: calc(var(--font-size-controls) * 0.85);
    font-weight: var(--font-weight-normal);
    white-space: nowrap;
}

.tag-filter-indicator-summary::before {
    content: '·';
    margin-right: var(--space-1, 0.28rem);
    opacity: 0.55;
}

.tag-filter-indicator-chip {
    cursor: pointer;
    transition:
        border-color 0.15s ease,
        background-color 0.15s ease,
        transform 0.15s ease;
}

.tag-filter-indicator-chip:hover,
.tag-filter-indicator-chip:focus-visible {
    border-color: var(--accent-primary);
    background: color-mix(in srgb, var(--accent-primary) 18%, var(--background-secondary));
    outline: none;
    transform: translateY(-1px);
}

.tag-filter-indicator-clear {
    flex-shrink: 0;
    border: 1px solid var(--border-primary);
    background: color-mix(in srgb, var(--background-secondary) 80%, transparent);
    color: var(--text-secondary);
    width: 1.55rem;
    height: 1.55rem;
    border-radius: var(--radius-pill, 999px);
    font-size: var(--text-md, 1rem);
    line-height: 1;
    cursor: pointer;
    transition:
        border-color 0.15s ease,
        color 0.15s ease,
        background-color 0.15s ease;
}

.tag-filter-indicator-clear:hover,
.tag-filter-indicator-clear:focus-visible {
    border-color: var(--accent-primary);
    color: var(--accent-primary);
    background: color-mix(in srgb, var(--background-primary) 94%, transparent);
    outline: none;
}

#dashboard-layout.tag-filter-view .tag-filter-view-body.dashboard-grid {
    width: 100%;
    max-width: 100%;
}

#dashboard-layout.tag-filter-view .tag-filter-view-body.dashboard-grid:not(.packed-columns) {
    width: fit-content;
    max-width: min(100%, var(--dashboard-grid-max-width, 100%));
    margin-inline: auto;
}

.category.tag-filter-chunk {
    gap: 0;
    width: 100%;
    max-width: 100%;
    min-width: 0;
    box-sizing: border-box;
}

/* Equal-width columns: the tag filter puts one chunk per column. */
#dashboard-layout.tag-filter-view .tag-filter-view-body.packed-columns {
    column-gap: var(--gap, 1.5rem);
}

#dashboard-layout.tag-filter-view .tag-filter-view-body.packed-columns .dashboard-column {
    flex: 1 1 0;
    flex-basis: min(
        var(--dashboard-column-max, 300px),
        calc((100% - (var(--packed-columns, 2) - 1) * var(--gap, 1.5rem)) / var(--packed-columns, 2))
    );
    max-width: min(
        var(--dashboard-column-max, 300px),
        calc((100% - (var(--packed-columns, 2) - 1) * var(--gap, 1.5rem)) / var(--packed-columns, 2))
    );
    width: min(
        var(--dashboard-column-max, 300px),
        calc((100% - (var(--packed-columns, 2) - 1) * var(--gap, 1.5rem)) / var(--packed-columns, 2))
    );
    min-width: 0;
}

#dashboard-layout.tag-filter-view .tag-filter-view-body:not(.packed-columns) .category.tag-filter-chunk {
    width: 100%;
    max-width: 100%;
}

#dashboard-layout.tag-filter-view .tag-filter-chunk .bookmarks-list {
    width: 100%;
    max-width: 100%;
}

/* Tag filter always uses vertical bookmark rows (overrides launcher tiles on parent grid) */
#dashboard-layout.tag-filter-view .tag-filter-chunk .bookmarks-list {
    display: grid;
    grid-template-columns: var(--bookmark-lead-col, minmax(1.25rem, max-content)) minmax(0, 1fr) max-content;
    row-gap: var(--space-2, 0.5rem);
    flex-direction: unset;
    flex-wrap: unset;
}

#dashboard-layout.tag-filter-view .tag-filter-chunk .bookmarks-list > * {
    grid-column: 1 / -1;
}

#dashboard-layout.tag-filter-view .tag-filter-chunk .bookmark-link {
    display: grid;
    grid-template-columns: subgrid;
    grid-column: 1 / -1;
    width: 100%;
    max-width: 100%;
    align-items: center;
    padding: 0.5rem var(--bookmark-row-end-pad, 3px) 0.5rem var(--bookmark-row-start-pad, 6px);
    min-height: 1.9rem;
    white-space: nowrap;
    border-radius: 0;
    gap: unset;
    flex-direction: unset;
}

#dashboard-layout.tag-filter-view .tag-filter-chunk .bookmark-link:hover {
    transform: none;
}

#dashboard-layout.tag-filter-view .tag-filter-chunk .bookmark-open::before {
    content: none;
}

#dashboard-layout.tag-filter-view .tag-filter-chunk .bookmark-reorder-handle {
    display: block !important;
}

#dashboard-layout.tag-filter-view .tag-filter-chunk .bookmark-shortcut {
    display: block !important;
    grid-column: 3;
    grid-row: 1;
    text-align: right;
    justify-self: end;
}

#dashboard-layout.tag-filter-view .tag-filter-chunk .bookmarks-list[data-show-ping="true"] {
    grid-template-columns:
        var(--bookmark-lead-col, minmax(1.25rem, max-content))
        minmax(0, 1fr)
        var(--bookmark-status-col, 3.35em)
        max-content;
}

#dashboard-layout.tag-filter-view .tag-filter-chunk .bookmarks-list[data-show-ping="true"] .bookmark-link > .status-text {
    grid-column: 3;
    grid-row: 1;
    justify-self: end;
    align-self: center;
}

#dashboard-layout.tag-filter-view .tag-filter-chunk .bookmarks-list[data-show-ping="true"] .bookmark-link > .bookmark-shortcut {
    grid-column: 4;
}

#dashboard-layout.tag-filter-view .tag-filter-chunk .bookmark-open-count {
    display: none !important;
}

#dashboard-layout.tag-filter-view .tag-filter-chunk .bookmark-pin-badge:not(.is-empty),
#dashboard-layout.tag-filter-view .tag-filter-chunk .bookmark-note-badge:not(.is-empty) {
    display: inline-flex !important;
}

#dashboard-layout.tag-filter-view .tag-filter-chunk .bookmark-lead {
    display: inline-flex;
    justify-content: flex-start;
    align-items: center;
    grid-column: 1;
    grid-row: 1;
    min-height: 1.15em;
    width: auto;
    min-width: 1.15em;
}

#dashboard-layout.tag-filter-view .tag-filter-chunk .bookmark-icon-slot {
    width: auto;
    height: auto;
}

#dashboard-layout.tag-filter-view .tag-filter-chunk .bookmark-icon {
    width: 1.15em;
    height: 1.15em;
}

#dashboard-layout.tag-filter-view .tag-filter-chunk a.bookmark-open {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    grid-column: 2;
    grid-row: 1;
    width: auto;
    min-width: 0;
    flex: unset;
    overflow: hidden;
}

#dashboard-layout.tag-filter-view .tag-filter-chunk .bookmark-text {
    font-size: var(--bookmark-font-size, 1rem);
    text-align: left;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    display: block;
    -webkit-line-clamp: unset;
    -webkit-box-orient: unset;
    line-height: inherit;
}

.empty-state--tag-filter {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-3, 0.75rem);
    padding: var(--space-5, 1.5rem) var(--space-3, 0.75rem);
    color: var(--text-tertiary);
    font-size: var(--font-size-text);
}

.empty-state--tag-filter-text {
    margin: 0;
    line-height: 1.5;
}

.empty-state--tag-filter-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2, 0.5rem);
}

.empty-state--tag-filter-btn {
    appearance: none;
    border: 1px solid var(--border-primary);
    border-radius: var(--header-chip-radius, 999px);
    background: color-mix(in srgb, var(--background-secondary) 55%, transparent);
    color: var(--text-secondary);
    padding: var(--space-1-5, 0.35rem) var(--space-3, 0.8rem);
    font: inherit;
    font-size: var(--font-size-controls);
    cursor: pointer;
    transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}

.empty-state--tag-filter-btn:hover,
.empty-state--tag-filter-btn:focus-visible {
    color: var(--text-primary);
    border-color: var(--text-primary);
    background: var(--background-secondary);
    outline: none;
}

/* ── Page navigation ─────────────────────────────────────────────────────── */
/*
 * The page track keeps a footprint, however many pages there are.
 *
 * It wrapped, so a twelfth page added a second row to the header and moved the
 * toolbar icons down with it -- the one part of the bar that should never move
 * was the part that moved most. It is one line now, bounded, and what does not
 * fit is counted on a chip at its end (see fitPageTabs in
 * dashboard-page-nav.js). The cap is a share of the header rather than a fixed
 * pixel width, so a narrow window gives the track less before the tabs
 * themselves have to shrink.
 */
.page-navigation {
    display: flex;
    flex-wrap: nowrap;
    gap: var(--space-1, 0.25rem);
    /*
     * The room that is left, not a share of the window.
     *
     * A viewport-relative cap let the track claim 52vw while the toolbar still
     * needed its own width, so at 900px the header ran out of room and wrapped
     * -- the actions dropped onto a second row, which is exactly what bounding
     * the track was meant to prevent. Shrinking is what the track does: it
     * gives up width first and folds the tabs that no longer fit.
     */
    flex: 0 1 auto;
    /*
     * A floor, so the track cannot be squeezed out of existence. Without it
     * the flex shrink took it to two pixels at 760px and the strip became a
     * chip with nothing beside it -- the active page has to stay visible, and
     * that needs room for one tab and the count.
     */
    min-width: min(9rem, 100%);
    overflow: hidden;
}

/* A tab that does not fit is out of the flow, not merely clipped: a clipped
   tab is still focusable, and tabbing to something you cannot see is worse
   than not having it. */
.page-nav-btn[hidden] {
    display: none;
}

/*
 * The count of what the track could not show.
 *
 * Reads as one of the tabs rather than as a control of its own -- it is the
 * rest of the same list -- and opens the page overview, which is where the
 * whole list lives.
 */
.page-nav-overflow {
    flex: 0 0 auto;
    padding: var(--space-1-5, 0.4rem) var(--space-2, 0.6rem);
    font-size: var(--font-size-controls);
    font-family: var(--font-family-main);
    font-weight: var(--font-weight-semibold);
    background: transparent;
    border: none;
    border-bottom: 2px solid transparent;
    color: var(--text-tertiary);
    cursor: pointer;
    transition: color 0.15s ease;
}

.page-nav-overflow:hover,
.page-nav-overflow:focus-visible {
    color: var(--text-primary);
}

.page-swipe-hint {
    display: none;
}

.page-nav-inbox-host {
    display: flex;
    align-items: center;
}

.page-nav-inbox-host:empty {
    display: none;
}

/*
 * Docker's nav icon renders into this host from JS (renderNavButton in
 * dashboard-docker-loader.js) only once a socket is known, so its styling
 * lives here in the base bundle rather than in docker-view.css — that sheet
 * only loads once the view itself is opened, which is too late for a button
 * that has to look right before anyone has clicked it.
 */
.page-nav-docker-host {
    display: flex;
    align-items: center;
}

.page-nav-docker-host:empty {
    display: none;
}

.page-nav-docker-host .docker-link-icon {
    width: 1.15rem;
    height: 1.15rem;
    flex-shrink: 0;
}

/* On a touch device wide enough to keep the desktop layout — a tablet, or a
   phone in landscape — the page tabs stay on screen and are tapped with a
   finger. They measured 28x33 there, under the 44px both iOS and Android ask
   for. The phone layout is unaffected: it hides #page-navigation outright and
   has its own 44px rules for the controls it keeps.

   Scoped to coarse pointers rather than applied everywhere, because on a mouse
   these are a dense row of tabs and padding them out to 44px would push the
   later pages off the strip for no gain. */
@media (hover: none) and (pointer: coarse) {
    /* The text styles set their tabs to their glyph's width; a finger needs more. */
    body:not([data-mobile-layout="true"]):is([data-page-switcher="text"], [data-page-switcher="classic"]) .header-track .page-nav-btn,
    body:not([data-mobile-layout="true"]) .page-nav-btn {
        min-height: 44px;
        min-width: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        touch-action: manipulation;
    }
}

.page-nav-btn {
    padding: var(--space-1-5, 0.4rem) var(--space-2, 0.6rem);
    font-size: var(--font-size-controls);
    font-family: var(--font-family-main);
    font-weight: var(--font-weight-semibold);
    background: transparent;
    border: none;
    border-bottom: 2px solid transparent;
    color: var(--text-secondary);
    cursor: pointer;
    transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
    text-align: center;
    position: relative;
}

.page-nav-btn::before,
.page-nav-btn::after {
    content: none;
}

.page-nav-btn:hover {
    color: var(--text-primary);
    background: color-mix(in srgb, var(--background-secondary) 60%, transparent 40%);
}

.page-nav-btn:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
}

.page-nav-btn.active {
    background: var(--background-secondary);
    color: var(--text-primary);
    border-bottom-color: var(--accent-primary);
    font-weight: var(--font-weight-bold);
}

.page-nav-btn.active::before,
.page-nav-btn.active::after {
    content: none;
}

.page-tab-icon {
    margin-right: 0.2em;
    font-style: normal;
    line-height: 1;
}

.page-tab-dot {
    display: inline-block;
    width: 0.45em;
    height: 0.45em;
    border-radius: 50%;
    margin-right: 0.35em;
    vertical-align: middle;
    flex-shrink: 0;
}

/* Page-tab edit popover (top/left set in JS from anchor + viewport) */
.page-tab-popover {
    position: fixed;
    z-index: 1100;
    background: var(--background-secondary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-6, 0.65rem);
    padding: var(--space-2, 0.6rem) var(--space-3, 0.7rem);
    box-shadow: 0 4px 18px rgba(0,0,0,0.22);
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 0.5rem);
    min-width: 12rem;
    max-width: min(20rem, calc(100vw - 1rem));
}

.page-tab-popover-row {
    display: flex;
    gap: var(--space-1-5, 0.4rem);
    align-items: center;
}

.page-tab-popover-icon {
    width: 2.6rem;
    text-align: center;
    font-size: 1.1em;
    background: var(--background-primary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-4, 0.4rem);
    color: var(--text-primary);
    padding: var(--space-1, 0.2rem) var(--space-1, 0.3rem);
    outline: none;
}
.page-tab-popover-icon:focus { border-color: var(--accent-primary); }

.page-tab-popover-name {
    flex: 1;
    background: var(--background-primary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-4, 0.4rem);
    color: var(--text-primary);
    font: inherit;
    font-size: var(--font-size-controls);
    padding: var(--space-1, 0.2rem) var(--space-1-5, 0.4rem);
    outline: none;
}
.page-tab-popover-name:focus { border-color: var(--accent-primary); }

.page-tab-color-swatches {
    display: flex;
    gap: var(--space-1, 0.3rem);
    flex-wrap: wrap;
}

.page-tab-color-swatch {
    width: 1.1rem;
    height: 1.1rem;
    border-radius: 50%;
    border: 2px solid transparent;
    cursor: pointer;
    padding: 0;
    transition: border-color 0.12s, transform 0.12s;
}
.page-tab-color-swatch:hover { transform: scale(1.2); }
.page-tab-color-swatch.selected { border-color: var(--accent-primary); }
.page-tab-color-swatch.swatch-none {
    border: 2px solid var(--border-primary);
    background: var(--background-primary) !important;
    position: relative;
}
.page-tab-color-swatch.swatch-none::after {
    content: '✕';
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--text-2xs, 0.6rem);
    color: var(--text-secondary);
    line-height: 1;
    pointer-events: none;
}

.page-tab-rename-input {
    background: transparent;
    border: none;
    border-bottom: 1px solid var(--text-primary);
    color: var(--text-primary);
    font: inherit;
    font-size: var(--font-size-controls);
    font-weight: var(--font-weight-semibold);
    padding: 0;
    margin: 0;
    width: 8ch;
    min-width: 4ch;
    max-width: 20ch;
    outline: none;
}

.library-link a,
.config-link a {
    text-decoration: none;
    font-size: var(--font-size-controls);
    font-family: var(--font-family-main);
    font-weight: var(--font-weight-semibold);
    transition: color 0.3s ease;
    color: var(--text-secondary);
}

.library-link a:hover,
.config-link a:hover {
    color: var(--text-primary);
}

.health-badge {
    position: absolute;
    top: -0.45em;
    right: -0.55em;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.15em;
    height: 1.15em;
    padding: 0 0.25em;
    font-size: 0.62em;
    font-weight: var(--font-weight-bold);
    font-variant-numeric: tabular-nums;
    line-height: 1;
    border-radius: var(--radius-pill, 999px);
    color: #fff;
    background: var(--accent-error);
    pointer-events: none;
    z-index: 1;
    box-sizing: border-box;
}

.health-badge-warn {
    background: var(--accent-warning);
    color: var(--text-on-warning, #000);
}

/* Icon variant: pages, health, config, and inbox (in the destination cluster)
   share one chrome; spacing is .header-actions / .header-destinations gap. */
.dashboard-link--icon,
.library-link--icon,
.config-link--icon {
    margin-left: 0;
}

.dashboard-link--icon a,
.library-link--icon a,
.config-link--icon a,
.header-destinations .page-nav-btn--inbox {
    animation: none;
    display: inline-flex;
    align-items: center;
    flex-wrap: nowrap;
    gap: var(--space-1, 0.3rem);
    padding: var(--space-1-5, 0.4rem) var(--space-2, 0.5rem);
    border-radius: var(--layout-radius-sm, 8px);
}

.dashboard-link--icon a:hover,
.library-link--icon a:hover,
.config-link--icon a:hover,
.header-destinations .page-nav-btn--inbox:hover {
    color: var(--text-primary);
    text-shadow: none;
}

/* Active state matched to .page-nav-btn.active: header destinations are the
   same kind of tab as page tabs, so "you are here" must look the same. */
.dashboard-link--icon a,
.library-link--icon a,
.config-link--icon a,
.header-destinations .page-nav-btn--inbox {
    border-bottom: 2px solid transparent;
}

.dashboard-link--icon a.active,
.library-link--icon a.active,
.config-link--icon a.active,
.header-destinations .page-nav-btn--inbox.active {
    background: var(--background-secondary);
    color: var(--text-primary);
    border-bottom-color: var(--accent-primary);
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}

.dashboard-link--icon .dashboard-link-icon,
.library-link--icon .library-link-icon,
.config-link--icon .config-link-icon,
.header-destinations .page-nav-btn--inbox .page-tab-icon--svg {
    width: 1.15rem;
    height: 1.15rem;
    display: inline-flex;
    align-items: center;
    color: currentColor;
    flex-shrink: 0;
}

.header-destinations .page-nav-btn--inbox {
    margin-left: 0;
}

/* Inline pill counter after the icon (overrides the absolute superscript dot). */
.library-link--icon .health-badge {
    position: static;
    top: auto;
    right: auto;
    min-width: 1.35rem;
    height: auto;
    padding: var(--space-hairline, 0.1rem) var(--space-1-5, 0.4rem);
    font-size: var(--text-xs, 0.72rem);
    font-weight: var(--font-weight-semibold);
    border-radius: var(--radius-pill, 999px);
    border: 1px solid color-mix(in srgb, var(--accent-error) 40%, transparent);
    background: color-mix(in srgb, var(--accent-error) 18%, transparent);
    color: var(--accent-error);
}

.library-link--icon .health-badge.health-badge-warn {
    border-color: color-mix(in srgb, var(--accent-warning) 45%, transparent);
    background: color-mix(in srgb, var(--accent-warning) 20%, transparent);
    color: var(--accent-warning);
}

/* The down badge shares broken's red — an outage is not a milder thing than a
   dead link. The two are told apart by movement, not hue: only the down state
   pulses the link (see .library-link.is-health-alert). Explicit here so the
   base .health-badge red still applies even beside the warn override above. */
.library-link--icon .health-badge.health-badge-down {
    border-color: color-mix(in srgb, var(--accent-error) 55%, transparent);
    background: color-mix(in srgb, var(--accent-error) 22%, transparent);
    color: var(--accent-error);
}

/* Fired once when the number of down monitors rises, then removed — a header
   ornament that pulsed forever would stop reading as an alert within a day.
   The cooldown and the rise check live in dashboard-visual.js; this is only the
   look of the pulse. */
@keyframes health-alert-pulse {
    0%   { transform: scale(1); }
    30%  { transform: scale(1.18); }
    60%  { transform: scale(0.96); }
    100% { transform: scale(1); }
}

.library-link.is-health-alert .library-link-anchor {
    animation: health-alert-pulse 0.5s cubic-bezier(0.22, 1, 0.36, 1) 3;
    transform-origin: center;
}

/* A quiet ring under the pulse, so a colour-blind or briefly-glanced view still
   catches it. Also removed with the class when the animation ends. */
.library-link.is-health-alert .health-badge {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-error) 30%, transparent);
}

body.no-animations .library-link.is-health-alert .library-link-anchor {
    animation: none !important;
}

@media (prefers-reduced-motion: reduce) {
    .library-link.is-health-alert .library-link-anchor {
        animation: none !important;
    }
}

/* Dashboard Grid */
.dashboard-grid {
    --dashboard-column-min: 250px;
    /* Upper bound a column may grow to. Raised from 300px so that on wide
       screens the columns absorb the available width (fewer, wider, more
       readable columns) instead of staying narrow and leaving large side
       whitespace. Below this width the per-column calc() takes over and the
       columns shrink gracefully toward --dashboard-column-min. */
    --dashboard-column-max: 360px;
    /* The horizontal space between two categories. Named because a wide
       category has to reproduce it between its own inner columns, and packed
       mode sets a different one. */
    --dashboard-column-gap: var(--space-5, 1.5rem);
    display: grid;
    row-gap: var(--category-row-gap, 2rem);
    column-gap: var(--dashboard-column-gap);
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    width: 100%;
    max-width: 100%;
}

#dashboard-layout.page-transition {
    animation: dashboardPageFade 200ms ease-out;
}

@keyframes dashboardPageFade {
    from {
        opacity: 0;
        transform: translateY(10px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* Opening a view is a jump to another place, not a turn of the page: the list
   is usually drawn within a frame, and 200 ms of fade and slide on top of that
   was most of what made Bookmarks feel slow to open. Half the time, no
   movement; page switches on the dashboard keep the slide. */
#dashboard-layout.page-transition:is(.config-layout, .library-layout, .inbox-layout, .docker-layout) {
    animation: dashboardViewFade 100ms ease-out;
}

@keyframes dashboardViewFade {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

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

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

.dashboard-grid.columns-3 {
    grid-template-columns: repeat(3, 1fr);
}

.dashboard-grid.columns-4 {
    grid-template-columns: repeat(4, 1fr);
}

.dashboard-grid.columns-5 {
    grid-template-columns: repeat(5, 1fr);
}

.dashboard-grid.columns-6 {
    grid-template-columns: repeat(6, 1fr);
}

.dashboard-grid.columns-3:not(.packed-columns) .category,
.dashboard-grid.columns-4:not(.packed-columns) .category,
.dashboard-grid.columns-5:not(.packed-columns) .category,
.dashboard-grid.columns-6:not(.packed-columns) .category {
    width: fit-content;
    max-width: 100%;
}

/* A category set wider than one column.
 *
 * `width: fit-content` above shrinks a category to its content, which is right
 * for a one-column block and wrong for this one: the whole point is to occupy
 * the columns it was given. Same specificity as the rule above and deliberately
 * placed after it, so this wins for the categories that carry the class.
 *
 * Grid only: this is the rule that reaches across grid columns. In packed mode
 * the same width is carried by a band of its own — see the packed block below —
 * and everything from the inner columns down is shared by both.
 */
.dashboard-grid:not(.packed-columns) .category.category--wide {
    grid-column: span var(--category-span, 1);
    width: 100%;
    max-width: 100%;
}

/* Every width, inner columns included, is driven by this one property, so
   putting it back to 1 is enough to undo the whole thing.
 *
 * `!important` because the effective width is written as an inline style at
 * render time and the window can be resized without a re-render — the grid
 * would otherwise still be told to span three columns it no longer has, and
 * spanning past the last column creates implicit ones and scrolls the page
 * sideways. */
body[data-dashboard-stack-categories="true"] .dashboard-grid .category.category--wide {
    --category-span: 1 !important;
}

.dashboard-grid.columns-1 .category.category--wide {
    --category-span: 1 !important;
}

/* One category, several columns — and no second rule to say so.
 *
 * A wide category used to draw its own line across the full span, under the
 * header. The header already carries one: the fading rule the depth presets
 * give every category title, which runs the same width because the title does.
 * Two lines a few pixels apart read as a mistake, so this one is gone and the
 * width is left to the spread badge and the layout itself.
 */

/* The marker in the header: how many columns this category is currently taking.
   Small and quiet — it is the one thing that says the width out loud, for the
   times the width alone is ambiguous (a narrow window, two columns of similar
   length). */
.category-spread-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.15em;
    padding: 0 0.3em;
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-3, 0.25rem);
    font-size: 0.7em;
    line-height: 1.5;
    color: var(--text-tertiary);
    white-space: nowrap;
}

/* Wider means more bookmarks side by side, not longer rows.
 *
 * A spread list is one plain track per column, and each row lays out its own
 * icon, name, status and shortcut inside the column it lands in. Rows fill row
 * by row, so the bookmarks flow left to right and then down.
 *
 * Not a subgrid of a repeated track pattern, which is what this used to be.
 * The seam between two columns has to be a column gap, and a grid has one
 * column gap for every track: it landed between icon and name and status as
 * well, and the subgrid handed the difference to its items as margin, so the
 * name track lost two seams' worth of width in every column and names a plain
 * category shows in full came out "Weather r…". The tracks a subgrid shared
 * across rows are lengths here instead: the status column always was one, and
 * the shortcut column is measured across the whole list and pinned — see
 * syncWideColumnTracks — so the rows of every column still line up.
 *
 * The launcher and list presets replace the list's display entirely, so these
 * declarations are inert there — their own wrapping already handles the width.
 *
 * Not scoped to either layout: how a wide category arranges its own bookmarks
 * is the same question whether the width came from spanning grid columns or
 * from a packed band.
 */
.dashboard-grid .category.category--wide .bookmarks-list {
    grid-template-columns: repeat(var(--category-span, 1), minmax(0, 1fr));
    /* The grid gap plus the padding two neighbouring categories would each
       have contributed.
     *
     * A run of ordinary categories pads every column on both sides; a wide one
     * is a single box and pads only its outer edges, so its inner columns drift
     * left of the columns above and below by exactly that missing padding. Add
     * it to the gap and the tracks land where a category's content lands.
     *
     * Only the column gap: the row gap belongs to the density setting, whose
     * rule sits further down this file and would otherwise win both. */
    column-gap: calc(var(--dashboard-column-gap, 1.5rem) + 2 * var(--category-inline-pad, 0.5rem));
    /* Resolved here, on the list, where syncWideColumnTracks writes them: a row
       sets --bookmark-lead-col on itself, so reading that name from the row
       would get the row's own default and never the measured length. */
    --wide-lead-col: var(--bookmark-lead-col, minmax(1.25rem, max-content));
    --wide-shortcut-col: var(--bookmark-shortcut-col, max-content);
    --wide-status-col: var(--bookmark-status-col, 3.35em);
}

/* A length, so the em in the status width resolves against the list, as it
   does in a plain category's tracks. Left unregistered it would reach the row
   as "3.35em" and be read against the row's larger bookmark font. */
@property --wide-status-col {
    syntax: '<length>';
    inherits: true;
    initial-value: 0px;
}

.dashboard-grid .category--wide .bookmarks-list > * {
    grid-column: span 1;
}

.dashboard-grid:not(.layout-list) .category--wide .bookmarks-list > .bookmark-link {
    grid-template-columns: var(--wide-lead-col) minmax(0, 1fr) var(--wide-shortcut-col);
}

/* The status track gives back what a plain row's subgrid gives back. There the
   tracks sit on the list's gap and the row's own, wider gap is made up as
   margin on the items beside each one: invisible on the icon and shortcut,
   whose tracks grow to hold it, but the status item comes out narrower than
   its track by the difference, and the name gets that room. A row with its
   own tracks keeps the full gap on both sides of the status column, so the
   column is narrowed by the same difference and the name ends where a plain
   one does. Without it "GitHub Pull Requests" fits in one column and not in
   a spread one, by four pixels. */
.dashboard-grid:not(.layout-list) .category--wide .bookmarks-list[data-show-ping="true"] > .bookmark-link {
    grid-template-columns:
        var(--wide-lead-col)
        minmax(0, 1fr)
        calc(var(--wide-status-col) - var(--bookmark-row-gap, 0px) + var(--bookmark-list-gap, 0px))
        var(--wide-shortcut-col);
}

/* A status row is inset 4px further than the rest (dashboard-bookmark-row.css),
   and in a plain category the shared lead track grows to take that in, so the
   names still line up. Rows that lay out their own tracks have nothing shared
   to grow: give the rows without the inset the same 4px after the icon, and
   the names start at one place down the column again. */
.dashboard-grid .category--wide .bookmarks-list[data-bookmarks-list="true"]:not([data-smart-collection="true"]):has(> .bookmark-link:is(.status-online, .status-offline, .status-checking):not(.bookmark-inline-editing))
    > .bookmark-link:not(:is(.status-online, .status-offline, .status-checking):not(.bookmark-inline-editing)) > .bookmark-lead {
    padding-inline-end: 4px;
}

/* The "+ N more" toggle and the empty-category placeholders are not bookmarks:
   they belong under the whole list, not in the first column of it. */
.dashboard-grid .category--wide .bookmarks-list > .category-show-more,
.dashboard-grid .category--wide .bookmarks-list > .empty-state--category,
.dashboard-grid .category--wide .bookmarks-list > .smart-collection-empty {
    grid-column: 1 / -1;
}

/* Packed columns: vertical stacks per column (round-robin), less vertical gap than CSS grid rows.
 *
 * This is the layout packed mode has always had, and it is what runs while no
 * category is wider than one column. A category set wider switches the page to
 * the masonry grid further down — a block cannot reach across flex columns it
 * is sitting inside of.
 */
.dashboard-grid.packed-columns {
    /* The width of a single packed column, written once — the masonry tracks
       below are the same width, so the two shapes line up. */
    --dashboard-column-gap: var(--gap, 1.5rem);
    --packed-column-width: min(
        var(--dashboard-column-max, 300px),
        calc((100% - (var(--packed-columns, 3) - 1) * var(--dashboard-column-gap)) / var(--packed-columns, 3))
    );
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: flex-start;
    justify-content: center;
    column-gap: var(--dashboard-column-gap);
    grid-template-columns: unset;
    width: 100%;
}

.dashboard-grid.packed-columns .dashboard-column {
    display: flex;
    flex-direction: column;
    gap: var(--space-7, 3rem);
    flex: 0 0 var(--packed-column-width);
    width: var(--packed-column-width);
    max-width: var(--packed-column-width);
    min-width: 0;
    contain: layout style;
}

/* Packed, with something wider than one column in it.
 *
 * Fixed columns cannot carry a block that reaches across them, and breaking the
 * page into bands left a hole the height of the tallest column beside the wide
 * one. So the page becomes a grid of very short rows: every category spans as
 * many of them as it is tall (set from JS, which is the only place a height is
 * known), and dense flow fills what is left — a wide block takes its columns
 * wherever it fits and the rest carry on beside and beneath it.
 *
 * align-self is what keeps the measuring honest: stretched to its row span, a
 * category would report the height it was given rather than the height it has,
 * and every pass would confirm the previous one.
 */
.dashboard-grid.packed-columns.packed-masonry {
    display: grid;
    grid-template-columns: repeat(var(--packed-columns, 3), var(--packed-column-width));
    grid-auto-rows: 4px;
    grid-auto-flow: row dense;
    column-gap: var(--dashboard-column-gap);
    row-gap: 0;
    justify-content: center;
    align-items: start;
    width: 100%;
}

.dashboard-grid.packed-columns.packed-masonry > .category {
    grid-row: span var(--masonry-span, 1);
    grid-column: span var(--category-span, 1);
    align-self: start;
    min-width: 0;
}

.dashboard-grid.packed-columns .category {
    width: 100%;
    box-sizing: border-box;
}

/* Center column block when the viewport is wider than the configured column count needs */
@media (min-width: 768px) {
    .dashboard-grid:not(.packed-columns):not(.layout-launcher) {
        width: fit-content;
        max-width: min(100%, var(--dashboard-grid-max-width, 100%));
        margin-inline: auto;
        justify-content: center;
    }

    .layout-launcher.packed-columns {
        justify-content: flex-start;
    }
}

.dashboard-grid.layout-compact.packed-columns .dashboard-column {
    gap: var(--space-5, 1.5rem);
}

.dashboard-grid.layout-cards.packed-columns .dashboard-column {
    gap: var(--space-5, 1.5rem);
}

.dashboard-grid.layout-terminal.packed-columns .dashboard-column {
    gap: var(--space-6, 2rem);
}

/* Layout presets */
.dashboard-grid.layout-default {
    row-gap: var(--category-row-gap, 2rem);
    column-gap: var(--space-5, 1.5rem);
}

.dashboard-grid.layout-compact {
    row-gap: var(--space-5, 1.5rem);
    column-gap: var(--space-4, 1rem);
}

.dashboard-grid.layout-compact .category {
    gap: var(--space-3, 0.65rem);
}

.dashboard-grid.layout-compact .bookmarks-list {
    --bookmark-list-gap: var(--space-1, 0.25rem);
    gap: var(--bookmark-list-gap);
}

.dashboard-grid.layout-compact .bookmark-link {
    padding: var(--space-1, 0.25rem) var(--bookmark-row-end-pad, 3px) var(--space-1, 0.25rem) var(--bookmark-row-start-pad, 6px);
}

.dashboard-grid.density-comfortable .bookmark-link {
    min-height: 2.05rem;
    padding-block: var(--density-row-pad-comfortable, 0.5rem);
}

.dashboard-grid.density-compact .bookmark-link {
    min-height: 1.85rem;
    padding-block: var(--density-row-pad-compact, 0.35rem);
}

.dashboard-grid.density-dense .bookmark-link {
    min-height: 1.65rem;
    padding-block: var(--density-row-pad-dense, 0.2rem);
    --bookmark-row-gap: var(--density-row-inner-gap-dense, 0.45rem);
    gap: var(--bookmark-row-gap);
}

/* auto: grid class density-auto had no row rules → smart vs normal drifted; align with compact baseline */
.dashboard-grid.density-auto .bookmark-link {
    min-height: 1.85rem;
    padding-block: var(--density-row-pad-compact, 0.35rem);
}

/* Inter-row gap scales with density for every bookmarks list (smart + reorderable). */
.dashboard-grid.density-comfortable .bookmarks-list {
    --bookmark-list-gap: var(--density-row-list-gap-comfortable, 0.42rem);
    gap: var(--bookmark-list-gap);
}
.dashboard-grid.density-compact .bookmarks-list {
    --bookmark-list-gap: var(--density-row-list-gap-compact, 0.3rem);
    gap: var(--bookmark-list-gap);
}
.dashboard-grid.density-dense .bookmarks-list {
    --bookmark-list-gap: var(--density-row-list-gap-dense, 0.18rem);
    gap: var(--bookmark-list-gap);
}
.dashboard-grid.density-auto .bookmarks-list {
    --bookmark-list-gap: var(--density-row-list-gap-compact, 0.3rem);
    gap: var(--bookmark-list-gap);
}

.dashboard-grid.layout-cards {
    gap: var(--space-5, 1.5rem);
}

.dashboard-grid.layout-cards .category {
    background: color-mix(in srgb, var(--background-primary) 92%, var(--text-secondary) 8%);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-6, 10px);
    padding: var(--space-4, 1rem);
}

.dashboard-grid.layout-cards .bookmark-link {
    border: none;
    border-radius: var(--layout-radius-sm, 8px);
    background: transparent;
    padding: var(--cards-pad-block, 0.45rem) 0.55rem;
}

.dashboard-grid.layout-cards .bookmark-open {
    align-items: flex-start;
}

.dashboard-grid.layout-cards .bookmark-text {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    line-clamp: 2;
    white-space: normal;
}

.dashboard-grid.layout-terminal {
    row-gap: var(--space-6, 2rem);
    column-gap: var(--space-5, 1.5rem);
}

/* Note badge tooltip */
.bookmark-note-badge {
    position: relative;
    pointer-events: auto;
    cursor: default;
}

.bookmark-note-badge[data-note-tooltip]::after {
    content: attr(data-note-tooltip);
    position: absolute;
    bottom: calc(100% + 6px);
    right: 0;
    width: max-content;
    max-width: 20rem;
    padding: var(--space-1, 0.3rem) var(--space-2, 0.55rem);
    border: 1px solid var(--border-primary);
    background: var(--background-secondary);
    color: var(--text-primary);
    border-radius: var(--layout-radius-sm, 8px);
    font-size: var(--text-xs, 0.72rem);
    white-space: normal;
    word-break: break-word;
    text-align: left;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.15s ease;
    z-index: 30;
}

.bookmark-note-badge[data-note-tooltip]:hover::after {
    opacity: 1;
}

/* Inline editor textarea for note */
.bookmark-inline-textarea {
    width: 100%;
    min-height: 3.2rem;
    padding: var(--space-1-5, 0.4rem);
    font-size: var(--text-base, 0.9rem);
    resize: vertical;
    font-family: inherit;
    border: 1px solid var(--border-primary);
    border-radius: var(--layout-radius-sm, 8px);
    background: var(--background-primary);
    color: var(--text-primary);
}

/*
 * Your own note, which is the one line on the card nobody else could have
 * written — and which used to be the faintest thing on it, in the same grey as
 * the site's marketing description.
 */
.bookmark-preview-card-note {
    margin: var(--space-2, 0.6rem) var(--space-3, 0.8rem) 0;
    padding: var(--space-1-5, 0.38rem) var(--space-2, 0.55rem);
    border-left: 2px solid var(--accent-primary);
    border-radius: 0 5px 5px 0;
    background: color-mix(in srgb, var(--accent-primary) 8%, transparent);
    color: var(--text-primary);
    font-size: calc(var(--font-size-controls) * 0.88);
    line-height: 1.4;
}

.bookmark-preview-card-note-label {
    display: block;
    margin-bottom: 3px;
    font-family: var(--font-family-mono, monospace);
    font-size: calc(var(--font-size-controls) * 0.66);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--accent-primary);
}

.bookmark-preview-card-note-text {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    overflow: hidden;
}

/* Tag autocomplete dropdown (appended to body by tag-autocomplete.js) */
.tag-ac-dropdown {
    position: fixed;
    z-index: 9999;
    margin: 0;
    padding: 0;
    list-style: none;
    background: var(--background-secondary);
    border: 1px solid var(--border-primary);
    max-height: 14rem;
    overflow-y: auto;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
}

.tag-ac-item {
    padding: var(--space-1-5, 0.35rem) var(--space-2, 0.6rem);
    font-family: inherit;
    font-size: var(--font-size-controls);
    color: var(--text-secondary);
    cursor: pointer;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.tag-ac-item strong {
    color: var(--text-primary);
    font-weight: var(--font-weight-semibold);
}

.tag-ac-item:hover,
.tag-ac-item-active {
    background: rgba(255, 255, 255, 0.06);
    color: var(--text-primary);
}

/* Preview card tags */
.bookmark-preview-card-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    padding: var(--space-2, 0.55rem) var(--space-3, 0.8rem) 0;
}

.dashboard-grid.layout-terminal .category-title-label::before {
    content: '$ ';
}

/* Terminal layout uses "$ " instead of "// "; hide the "//" handle. */
.dashboard-grid.layout-terminal .category-reorder-handle {
    display: none;
}

.dashboard-grid.layout-terminal .bookmark-link {
    font-family: "Source Code Pro", monospace;
    letter-spacing: 0.01em;
    --bookmark-row-gap: var(--space-1-5, 0.35rem);
    gap: var(--bookmark-row-gap);
    padding-left: var(--space-4, 1rem);
    white-space: nowrap;
    overflow: hidden;
}

.dashboard-grid.layout-terminal .bookmark-link::before {
    content: '> ';
    opacity: 0.65;
    margin-right: 0;
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    line-height: 1;
    pointer-events: none;
}

.dashboard-grid.layout-terminal .bookmark-open {
    min-width: 0;
    overflow: hidden;
}

.dashboard-grid.layout-terminal .bookmark-text {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.dashboard-grid.layout-terminal .bookmark-shortcut {
    width: auto;
}

/* New layout presets: masonry, list, widgets */
.dashboard-grid.layout-masonry {
    gap: var(--space-4, 1rem);
}
.dashboard-grid.layout-masonry .category {
    /* allow variable-height cards and denser packing */
    gap: var(--space-3, 0.75rem);
}
.dashboard-grid.layout-masonry .bookmark-open {
    align-items: stretch;
}

.dashboard-grid.layout-list {
    gap: var(--space-2, 0.6rem);
}
.dashboard-grid.layout-list .category {
    background: transparent;
    border: none;
    padding: 0;
}
.dashboard-grid.layout-list .bookmarks-list {
    display: block;
}
.dashboard-grid.layout-list .bookmark-link {
    grid-template-columns: var(--bookmark-lead-col) minmax(0, 1fr) max-content;
    padding: var(--space-2, 0.6rem) var(--space-1-5, 0.4rem);
    border-bottom: 1px solid var(--border-primary);
}
.dashboard-grid.layout-list .bookmark-text {
    white-space: normal;
}

.dashboard-grid.layout-widgets {
    gap: var(--space-5, 1.5rem);
}
.dashboard-grid.layout-widgets .category {
    background: color-mix(in srgb, var(--background-primary) 94%, var(--text-secondary) 6%);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-7, 12px);
    padding: var(--space-4, 1rem);
}
.dashboard-grid.layout-widgets .bookmark-link {
    padding: var(--space-2, 0.6rem);
    border: none;
    border-radius: var(--layout-radius-sm, 8px);
    background: transparent;
}

/* Preset-driven CSS variables applied via body attributes */
.dashboard-grid {
    gap: var(--gap, 1.5rem);
    font-size: var(--dashboard-font-size, 14px);
}
.dashboard-grid .bookmark-link {
    padding-block: var(--card-padding, 0.5rem);
}
.dashboard-grid .bookmark-text {
    font-size: var(--bookmark-font-size, 1rem);
}

/* Layout presets: set default variable values */
body[data-layout-preset="default"] .dashboard-grid {
    --gap: var(--space-7, 3rem);
    --card-padding: var(--space-4, 0.9rem);
    --dashboard-font-size: 14px;
    --bookmark-font-size: var(--text-md, 1rem);
}
body[data-layout-preset="compact"] .dashboard-grid {
    --gap: var(--space-5, 1.5rem);
    --card-padding: var(--space-2, 0.45rem);
    --dashboard-font-size: 13px;
    --bookmark-font-size: var(--text-md, 0.98rem);
}
body[data-layout-preset="cards"] .dashboard-grid {
    --gap: var(--space-5, 1.5rem);
    --card-padding: var(--space-4, 0.9rem);
    --dashboard-font-size: 14px;
}
body[data-layout-preset="terminal"] .dashboard-grid {
    --gap: var(--space-6, 2rem);
    --card-padding: var(--space-1-5, 0.35rem);
    --dashboard-font-size: 13px;
}
body[data-layout-preset="masonry"] .dashboard-grid {
    --gap: var(--space-4, 1rem);
    --card-padding: var(--space-2, 0.6rem);
    --dashboard-font-size: 14px;
}
body[data-layout-preset="list"] .dashboard-grid {
    --gap: var(--space-2, 0.6rem);
    --card-padding: var(--space-2, 0.6rem);
    --dashboard-font-size: 14px;
}
body[data-layout-preset="widgets"] .dashboard-grid {
    --gap: var(--space-5, 1.5rem);
    --card-padding: var(--space-4, 1rem);
    --dashboard-font-size: 15px;
}
body[data-layout-preset="launcher"] .dashboard-grid {
    --gap: var(--space-1-5, 0.4rem);
    --card-padding: var(--space-2, 0.5rem);
    --dashboard-font-size: 14px;
}

/* ─── Launcher layout preset ─────────────────────────────────── */
.dashboard-grid.layout-launcher,
.dashboard-grid.layout-launcher.packed-columns {
    display: flex;
    flex-direction: column;
    gap: var(--space-6, 2rem);
}

/* With packed-columns: dissolve .dashboard-column wrappers so     */
/* category-sections participate directly in the launcher flex     */
.layout-launcher.packed-columns .dashboard-column {
    display: contents;
}

.layout-launcher .category {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-1, 0.2rem);
    padding: 0;
}

/* Widgets are .category too, but have no tile row to hug: flex-start left
   their body as wide as its widest word, which reads as an empty dashboard. */
.layout-launcher .category.dashboard-widget {
    align-items: stretch;
}

.layout-launcher .category-title {
    text-align: left;
    white-space: nowrap;
    padding: 0;
    cursor: default;
}

.layout-launcher .bookmarks-list {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    gap: var(--space-2, 0.6rem);
    width: 100%;
}

.layout-launcher .bookmark-link {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    width: 88px;
    padding: var(--space-3, 0.65rem) var(--space-2, 0.5rem) var(--space-2, 0.5rem);
    border-radius: var(--radius-6, 10px);
    gap: var(--space-1-5, 0.4rem);
    cursor: pointer;
    transition: background 0.12s ease, transform 0.1s ease;
    /* prevent subgrid/grid-row from base styles bleeding in */
    grid-template-columns: none;
    min-height: 0;
    white-space: normal;
}
.layout-launcher .bookmark-link:hover {
    transform: translateY(-2px);
}

/* Stretched-link: transparent ::before overlay covers the whole tile so
   clicking the icon or anywhere on the tile triggers the <a> */
.layout-launcher .bookmark-open::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: var(--radius-6, 10px);
    z-index: 1;
}

.layout-launcher .bookmark-reorder-handle,
.layout-launcher .bookmark-shortcut,
.layout-launcher .bookmark-note-badge,
.layout-launcher .bookmark-pin-badge,
.layout-launcher .bookmark-open-count { display: none !important; }

.layout-launcher .bookmark-lead {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 48px;
    width: 48px;
    flex-shrink: 0;
}
.layout-launcher .bookmark-icon-slot {
    display: flex;
    width: 48px;
    height: 48px;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}
.layout-launcher .bookmark-icon {
    width: 48px;
    height: 48px;
    object-fit: contain;
}

.layout-launcher .bookmark-open {
    display: flex;
    justify-content: center;
    width: 100%;
    overflow: visible;
    min-width: 0;
    flex: 0 0 auto;
}
.layout-launcher .bookmark-text {
    font-size: var(--text-xs, 0.7rem);
    text-align: center;
    white-space: normal;
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    line-height: 1.3;
}

/* ─── Launcher icon sizes ─────────────────────────────────────── */
body[data-launcher-icon-size="small"] .layout-launcher .bookmark-link { width: 68px; }
body[data-launcher-icon-size="small"] .layout-launcher .bookmark-lead { min-height: 32px; width: 32px; }
body[data-launcher-icon-size="small"] .layout-launcher .bookmark-icon-slot { width: 32px; height: 32px; }
body[data-launcher-icon-size="small"] .layout-launcher .bookmark-icon { width: 32px; height: 32px; }

body[data-launcher-icon-size="large"] .layout-launcher .bookmark-link { width: 108px; }
body[data-launcher-icon-size="large"] .layout-launcher .bookmark-lead { min-height: 64px; width: 64px; }
body[data-launcher-icon-size="large"] .layout-launcher .bookmark-icon-slot { width: 64px; height: 64px; }
body[data-launcher-icon-size="large"] .layout-launcher .bookmark-icon { width: 64px; height: 64px; }

/* ─── Launcher dim (search filter) ───────────────────────────── */
.layout-launcher .bookmark-link.launcher-dim {
    opacity: 0.15;
    filter: grayscale(1);
    transition: opacity 0.18s ease, filter 0.18s ease;
    pointer-events: none;
}
.layout-launcher .bookmark-link:not(.launcher-dim) {
    transition: opacity 0.18s ease, filter 0.18s ease;
}

/* ─── Corner FAB layout tokens ───────────────────────────────────────────── */
:root {
    --corner-fab-edge: 1.5rem;
    --corner-fab-height: 2.75rem;
    --corner-fab-gap: var(--space-1-5, 0.4rem);
}

@media (max-width: 768px), (hover: none) and (pointer: coarse) {
    :root {
        --corner-fab-height: 3.35rem;
    }
}

/* ─── What's new FAB ──────────────────────────────────────────────────────── */
.whats-new-btn {
    position: fixed;
    bottom: var(--corner-fab-edge);
    /* Right, not left: the left corner is where the page's own content starts,
       and the actions it belongs beside all sit at the right end of the bar. */
    right: var(--corner-fab-edge);
    left: auto;
    z-index: 1001;
    padding: var(--space-2, 0.62rem) var(--space-3, 0.72rem);
    border: none;
    /* Opaque, and in both layout versions: this FAB floats over the grid with
       nothing behind it either way -- modern styles the dock and the / FAB but
       never this one -- so the page used to read straight through it. The wash
       is the same 74%, now composited over the page background instead of over
       transparent, which is the colour it already showed over empty page. */
    background: color-mix(in srgb, var(--background-secondary) 74%, var(--background-primary));
    box-shadow: var(--action-btn-shadow);
    color: var(--accent-primary);
    font-size: var(--font-size-controls);
    font-family: var(--font-family-main);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-6, 0.65rem);
    transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
    -webkit-tap-highlight-color: transparent;
    user-select: none;
}
.whats-new-btn-icon {
    display: block;
    flex-shrink: 0;
}
.whats-new-btn-update-dot {
    position: absolute;
    top: 0.35rem;
    right: 0.35rem;
    width: 0.45rem;
    height: 0.45rem;
    border-radius: 50%;
    background: var(--accent-primary);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--background-secondary) 80%, transparent);
    pointer-events: none;
    opacity: 0;
    transform: scale(0);
    transition: opacity 0.2s ease, transform 0.2s ease;
}
.whats-new-btn.has-update-available .whats-new-btn-update-dot {
    opacity: 1;
    transform: scale(1);
    animation: whats-new-update-pulse 1.8s ease-in-out infinite;
}
@keyframes whats-new-update-pulse {
    0%, 100% {
        transform: scale(1);
        box-shadow:
            0 0 0 2px color-mix(in srgb, var(--background-secondary) 80%, transparent),
            0 0 0 0 color-mix(in srgb, var(--accent-primary) 0%, transparent);
    }
    50% {
        transform: scale(1.2);
        box-shadow:
            0 0 0 2px color-mix(in srgb, var(--background-secondary) 80%, transparent),
            0 0 0 5px color-mix(in srgb, var(--accent-primary) 38%, transparent);
    }
}
body.no-animations .whats-new-btn-update-dot {
    animation: none;
    transform: scale(1);
}
.whats-new-btn:hover {
    /* 94% of the page background over the page background is the page
       background: same colour as before, minus the window. */
    background: var(--background-primary);
    transform: translateY(-1px);
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16),
        0 0 0 1px color-mix(in srgb, var(--accent-primary) 45%, transparent);
}
.whats-new-btn:active {
    transform: translateY(0);
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
}
/* Tooltip */
.whats-new-btn[data-tooltip] {
    position: fixed;
}
.whats-new-btn[data-tooltip]::after {
    content: attr(data-tooltip);
    position: absolute;
    bottom: calc(100% + 6px);
    /* Hung from the same edge the button is pinned to, so it opens inward
       instead of off the screen. */
    right: 0;
    left: auto;
    white-space: nowrap;
    padding: var(--space-1, 0.2rem) var(--space-2, 0.5rem);
    background: var(--background-secondary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-3, 0.3rem);
    color: var(--text-secondary);
    font-family: var(--font-family-main);
    font-size: calc(var(--font-size-controls) * 0.85);
    font-weight: var(--font-weight-normal);
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
    pointer-events: none;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.15s ease, visibility 0.15s ease;
    z-index: 1001;
}
.whats-new-btn[data-tooltip]:hover::after {
    opacity: 1;
    visibility: visible;
}
@media (pointer: coarse) {
    .whats-new-btn[data-tooltip]::after {
        display: none;
    }
}

/* Density overrides (can be combined with layout presets) */
body[data-density-mode="comfortable"] .dashboard-grid {
    --card-padding: 1.2rem;
    --dashboard-font-size: 16px;
    --cards-pad-block: 0.6rem;
}
body[data-density-mode="compact"] .dashboard-grid {
    --card-padding: var(--space-3, 0.8rem);
    --dashboard-font-size: 14px;
    --cards-pad-block: 0.45rem;
}
body[data-density-mode="dense"] .dashboard-grid {
    --card-padding: var(--space-2, 0.45rem);
    --dashboard-font-size: 13px;
    --cards-pad-block: 0.3rem;
}
/* 'auto' leaves variables to layout + responsive CSS to decide */

/* Vertical space between category rows.
 *
 * Distinct from density above, which sizes the bookmark rows inside a category:
 * this is the gap between the rows of categories themselves, which is what
 * makes a wide page feel empty. The old fixed 3rem is still available as
 * "airy"; the default drops to 2rem, because on a five-column layout 3rem left
 * a visible band of nothing between every row.
 *
 * The variable is read by .dashboard-grid.layout-default and the auto-fit grid;
 * the packed and compact presets keep their own tighter gaps. */
body[data-category-spacing="snug"] {
    --category-row-gap: 1.25rem;
}
body[data-category-spacing="balanced"] {
    --category-row-gap: var(--space-6, 2rem);
}
body[data-category-spacing="airy"] {
    --category-row-gap: var(--space-7, 3rem);
}

/* Keep terminal rows single-line in dense layouts (e.g. 6 columns). */
.dashboard-grid.layout-terminal .status-text {
    display: none !important;
}

/* Category */
.category {
    display: flex;
    flex-direction: column;
    gap: var(--space-4, 1rem);
    min-width: 0; /* Allow flex items to shrink below content size */
    /* Named, because a wide category has to add it back between its inner
       columns — see the column-gap on its bookmarks list. */
    --category-inline-pad: var(--space-2, 0.5rem);
    padding-left: var(--category-inline-pad);
    padding-right: var(--category-inline-pad);
}

.category.animate-enter {
    opacity: 0;
    transform: translateY(8px);
    animation: categoryEnter 140ms ease-out forwards;
    animation-delay: calc(80ms + var(--stagger-index, 0) * 6ms);
}

@keyframes categoryEnter {
    from {
        opacity: 0;
        transform: translateY(8px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* Centred rather than baseline-aligned, which is what keeps the controls in the
   header from making it taller. On a baseline the "+" sits by its own text
   baseline, and the padding and border below that baseline hang past the line
   the title occupies — 1.4px of extra header, per header, for a button that is
   shorter than the text beside it. Centring aligns boxes instead, so the header
   is simply as tall as its tallest child and a control that fits costs nothing.
   Titles that wrap keep flex-start (see .category-title--multiline): chips
   belong beside the first line there, not centred across both. */
/* Categoriekoppen delen de pagina in, en stonden op --text-tertiary: de
   zwakste kleur die een theme heeft. Op --text-secondary blijven ze
   ondergeschikt aan de bookmarknamen zonder als eerste weg te vallen. */
.category-title {
    font-size: var(--font-size-category);
    font-weight: var(--font-weight-bold);
    letter-spacing: 0.05em;
    color: var(--text-secondary);
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    min-width: 0;
}

.category-title-label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    /* Room for the focus tint on the // (below): overflow: hidden cut an
       outline off on the left and the bottom. Padding and the same negative
       margin, so nothing in the title moves. */
    padding: 2px 0 2px 0.2em;
    margin: -2px 0 -2px -0.2em;
}

/* The "//" prefix is now a real element that doubles as the drag-reorder handle. */
.category-reorder-handle {
    /* A button for the pointer and for assistive tech, looking like the plain "//" it replaced. */
    appearance: none;
    background: none;
    border: 0;
    padding: 0;
    margin: 0;
    font: inherit;
    color: inherit;
    cursor: grab;
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
    white-space: pre;
    opacity: 0.55;
    transition: opacity 0.15s ease;
}

.category-title:hover .category-reorder-handle {
    opacity: 0.85;
}

.category-reorder-handle:hover {
    opacity: 1;
}

.category-reorder-handle:active {
    cursor: grabbing;
}

/* The two slashes, which the focus tint sits on. */
.category-reorder-glyph {
    border-radius: 4px;
    padding: 0 0.2em;
    margin-left: -0.2em;
}

/*
 * Reached by Tab: it has to show where focus is, in every header style. In
 * the tint of the row the cursor is on, inside the slashes' own box, rather
 * than an outline around the button: that took the space after // and the
 * title's letter spacing along, and the label's overflow cut it to two sides.
 */
.category-reorder-handle:focus-visible {
    opacity: 1;
    outline: none;
}

.category-reorder-handle:focus-visible .category-reorder-glyph {
    background: var(--bookmark-row-selected-bg);
    color: var(--text-primary);
}

/* Other and an unknown category: the "//" for the look, nothing to pick up. */
.category-reorder-handle.is-static {
    cursor: default;
}

/* Lock layout setting: the "//" handle stays put instead of disappearing
   (removing it would shift every category title), but no longer offers a
   grab cursor for a drag that will not happen. */
body.layout-locked .category-reorder-handle {
    cursor: default;
}

/* Moving a block: the page holds still and a box shows where it lands. */
body.is-moving-block { cursor: grabbing; user-select: none; }
.category.is-move-source { opacity: .35; outline: 1px dashed var(--border-primary); outline-offset: -1px; }
.block-landing {
    position: absolute; z-index: 4; pointer-events: none; box-sizing: border-box;
    border: 2px dashed var(--accent-primary);
    background: color-mix(in srgb, var(--accent-primary) 12%, transparent);
    transition: left .12s ease, top .12s ease, width .12s ease, height .12s ease;
}
.block-landing-label {
    position: absolute; top: 0; left: 0; padding: 2px 7px;
    font: 600 var(--font-size-small, 11px) var(--font-mono, monospace);
    background: var(--accent-primary); color: var(--background-primary); white-space: nowrap;
}
.block-landing-width { pointer-events: auto; position: absolute; right: 4px; top: 4px; }
.block-seam {
    position: absolute; top: 0; bottom: 0; width: 52px; margin-left: -26px; z-index: 3; pointer-events: none;
    background: repeating-linear-gradient(0deg, color-mix(in srgb, var(--accent-primary) 16%, transparent) 0 6px, transparent 6px 12px);
}
.block-drag-chip {
    position: fixed; left: 0; top: 0; z-index: 1000; pointer-events: none;
    padding: 6px 10px; border: 1px solid var(--accent-primary); background: var(--background-secondary);
    color: var(--accent-primary); font: 600 12px var(--font-mono, monospace);
    box-shadow: 0 10px 30px rgb(0 0 0 / .45);
}
/* First-time coach: beside the landing box while a block is carried, then beside
   the block until "Got it". Fixed, so it can stay inside the viewport. It takes no
   pointer events while carried (never the target of a drop). */
.block-move-coach {
    position: fixed; left: 0; top: 0; z-index: 900; box-sizing: border-box;
    width: min(22rem, calc(100vw - 16px)); padding: 15px 16px;
    border: 2px solid var(--accent-primary); border-radius: var(--radius-3, 6px);
    background: var(--background-secondary); color: var(--text-secondary);
    box-shadow: 0 16px 48px rgb(0 0 0 / .55), 0 0 0 4px color-mix(in srgb, var(--accent-primary) 18%, transparent);
    font-size: var(--text-sm, 0.8rem); line-height: 1.45;
    animation: block-coach-in .18s ease-out;
}
.block-move-coach.is-carried { pointer-events: none; }
.block-move-coach-eyebrow {
    margin-bottom: 2px; color: var(--accent-primary); font-size: var(--text-2xs, 0.68rem);
    font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
}
.block-move-coach-title { margin-bottom: 6px; color: var(--text-primary); font-weight: 600; font-size: 15px; }
.block-move-coach p { margin: 0 0 4px; }
.block-move-coach-keys {
    display: flex; flex-wrap: wrap; gap: var(--space-1-5, 0.35rem) var(--space-3, 0.75rem);
    margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border-secondary);
    font-size: var(--text-xs, 0.75rem); color: var(--text-tertiary);
}
.block-move-coach kbd {
    font-size: var(--text-xs, 0.7rem); padding: var(--space-hairline, 0.05rem) var(--space-1-5, 0.32rem);
    border-radius: var(--radius-3, 0.25rem); border: 1px solid var(--border-secondary);
    background: var(--background-primary); color: var(--text-secondary);
}
.block-move-coach-done {
    display: block; margin: 12px 0 0 auto; padding: 5px 14px; cursor: pointer;
    border: 1px solid var(--accent-primary); border-radius: var(--radius-3, 4px);
    background: var(--accent-primary); color: var(--background-primary); font-family: inherit; font-weight: 600; font-size: var(--text-sm, 0.8rem);
}
.block-move-coach-done:focus-visible { outline: 2px solid var(--text-primary); outline-offset: 2px; }
@keyframes block-coach-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .block-move-coach { animation: none; } }
/* One-time intro that shows blocks can be moved: a see-through copy of the first
   category glides to the next place, the landing box and column lines show, and a
   card stays under its "//". Nothing here takes a click except the card's buttons. */
.block-intro-ghost {
    position: absolute; z-index: 5; pointer-events: none; box-sizing: border-box; opacity: 0;
    padding: var(--space-2, 0.5rem); overflow: hidden;
    border: 1px solid var(--accent-primary); background: var(--background-secondary);
    color: var(--accent-primary); font-family: inherit; font-size: var(--font-size-category);
    font-weight: var(--font-weight-bold); letter-spacing: 0.05em; white-space: nowrap;
    box-shadow: 0 14px 34px rgb(0 0 0 / .45);
    transition: left .7s cubic-bezier(.2, .7, .2, 1), top .7s cubic-bezier(.2, .7, .2, 1), width .7s cubic-bezier(.2, .7, .2, 1), opacity .25s, transform .25s;
}
.block-intro-ghost.is-up { opacity: .92; transform: translateY(-6px) scale(1.02); }
.block-landing.block-intro-box { animation: block-coach-in .25s ease-out; transition: left .35s, top .35s, width .35s, height .35s; }
.category-reorder-handle.is-intro-glow { opacity: 1; color: var(--accent-primary); animation: block-intro-glow .7s ease-in-out 2; }
.category-reorder-handle.is-intro-ring { opacity: 1; color: var(--accent-primary); border-radius: 2px; box-shadow: 0 0 0 2px var(--accent-primary); }
@keyframes block-intro-glow {
    50% { text-shadow: 0 0 10px var(--accent-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-primary) 35%, transparent); }
}
.block-intro-card { --intro-arrow-x: 18px; }
.block-intro-card::before {
    content: ""; position: absolute; left: var(--intro-arrow-x); top: -9px; width: 14px; height: 14px;
    background: var(--background-secondary); border-left: 2px solid var(--accent-primary); border-top: 2px solid var(--accent-primary);
    transform: rotate(45deg);
}
.block-intro-card.is-above::before { top: auto; bottom: -9px; transform: rotate(225deg); }
.block-intro-card code { color: var(--accent-primary); }
.block-intro-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }
.block-intro-actions .block-move-coach-done { display: inline-block; margin: 0; }
.block-intro-btn {
    padding: 5px 14px; cursor: pointer; border: 1px solid var(--border-secondary); border-radius: var(--radius-3, 4px);
    background: var(--background-primary); color: var(--text-primary); font-family: inherit; font-weight: 600; font-size: var(--text-sm, 0.8rem);
}
.block-intro-btn:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
    .block-intro-ghost, .block-landing.block-intro-box { transition: none; animation: none; }
    .category-reorder-handle.is-intro-glow { animation: none; }
}
.category.is-move-landed { box-shadow: 0 0 0 2px color-mix(in srgb, var(--status-success, #4fd18b) 45%, transparent); transition: box-shadow .6s; }
@media (prefers-reduced-motion: reduce) { .block-landing { transition: none; } }

.category-title-trailing {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: baseline;
    flex-shrink: 0;
    white-space: nowrap;
}

/*
 * inline-block is what makes everything under it work, and the fit besides.
 *
 * This is a span, and an inline box has no width of its own: overflow and
 * text-overflow are ignored on one, and scrollWidth and clientWidth both read
 * 0. dashboard-category-title-fit.js decides whether to step the font down or
 * wrap to two lines by measuring exactly those, so it measured nothing, and
 * every title was left at full size to run past its column until
 * .category-title-label's overflow: hidden cut it mid-word with no ellipsis --
 * "unknown category (testing)" read as "unknown category (testi".
 *
 * inline-block rather than block, which would be just as wrong the other way:
 * a block fills its container, so every name would measure as exactly too wide
 * and every title in the grid would shrink to the floor and wrap. The width has
 * to be the text's own, and max-width keeps it inside the column.
 */
.category-title-name {
    display: inline-block;
    max-width: 100%;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.category-title-name--multiline {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: normal;
    word-break: break-word;
}

.category-title--multiline {
    align-items: flex-start;
}

.category-chevron {
    flex-shrink: 0;
}

/* One button plus a ⋯, not the full set: four sort buttons on every category
   header — and the header repeats per category — took more width than the
   bookmark names beside them. In manual order there is no sort to report, so
   only the ⋯ is left. */
.category-sort-controls {
    display: inline-flex;
    align-items: center;
    gap: 0.2em;
    margin-left: 0.35em;
    flex-shrink: 0;
    opacity: 1;
    pointer-events: auto;
    position: relative;
}

.category-sort-menu-btn {
    font: inherit;
    font-size: 0.68em;
    line-height: 1;
    min-height: 1.45em;
    min-width: 1.45em;
    padding: 0.15em 0.4em;
    border: none;
    border-radius: 50%;
    background: var(--action-btn-bg);
    color: var(--action-btn-color);
    cursor: pointer;
    opacity: 0.85;
}

.category-sort-menu-btn:hover,
.category-sort-menu-btn:focus-visible,
.category-sort-menu-btn[aria-expanded="true"] {
    opacity: 1;
    color: var(--text-primary);
    background: var(--action-btn-bg-hover);
}

.category-sort-menu {
    position: absolute;
    top: calc(100% + 0.25em);
    right: 0;
    z-index: 60;
    display: flex;
    flex-direction: column;
    min-width: 7em;
    padding: 0.2em;
    border: 1px solid var(--border-primary);
    border-radius: var(--layout-radius-sm, 6px);
    background: var(--background-primary);
    box-shadow: 0 6px 18px color-mix(in srgb, var(--text-primary) 14%, transparent);
}

.category-sort-menu-item {
    font: inherit;
    font-size: 0.68em;
    text-align: left;
    padding: 0.3em 0.5em;
    border: 0;
    border-radius: var(--radius-2, 3px);
    background: none;
    color: var(--text-secondary);
    cursor: pointer;
    white-space: nowrap;
}

.category-sort-menu-item:hover,
.category-sort-menu-item:focus-visible {
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
    color: var(--text-primary);
}

.category-sort-menu-item.is-active {
    color: var(--text-primary);
    font-weight: var(--font-weight-bold);
}

.category-sort-menu-item.is-active::before {
    content: '· ';
}

.category-sort-btn {
    font: inherit;
    font-size: 0.68em;
    font-weight: var(--font-weight-semibold);
    letter-spacing: 0.02em;
    line-height: 1.2;
    min-height: 1.45em;
    padding: 0.15em 0.55em;
    border: none;
    border-radius: var(--radius-pill, 999px);
    background: var(--action-btn-bg);
    color: var(--action-btn-color);
    cursor: pointer;
    opacity: 0.9;
}

.category-sort-btn:hover {
    opacity: 1;
    color: var(--text-primary);
    background: var(--action-btn-bg-hover);
}

.category-sort-btn.is-active,
.category-sort-controls[data-sort-mode="az"] .category-sort-btn[data-sort-mode="az"],
.category-sort-controls[data-sort-mode="recent"] .category-sort-btn[data-sort-mode="recent"] {
    opacity: 1;
    color: var(--text-primary);
    font-weight: var(--font-weight-bold);
    border-color: color-mix(in srgb, var(--accent-color, currentColor) 65%, transparent);
    background: color-mix(in srgb, var(--accent-color, currentColor) 20%, var(--background-secondary, transparent));
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-color, currentColor) 28%, transparent);
    text-decoration: none;
}

.category-sort-controls[data-sort-mode="order"] .category-sort-btn.is-active {
    opacity: 0.9;
    font-weight: var(--font-weight-semibold);
    color: var(--text-tertiary);
    border-color: color-mix(in srgb, var(--text-tertiary) 28%, transparent);
    background: color-mix(in srgb, var(--background-secondary, transparent) 55%, transparent);
    box-shadow: none;
}

.category-sort-btn + .category-sort-btn::before {
    content: none;
}

.bookmarks-list.bookmarks-list--sort-active .bookmark-reorder-handle {
    display: none;
}

/* Manual drag is off while A–Z / Recent sorting owns the order. Signal it: no grab
   affordance, and a not-allowed cursor on a press-drag. A plain click still opens
   the bookmark (the link keeps its own pointer cursor). */
.bookmarks-list.bookmarks-list--sort-active .bookmark-link.reorder-item:active {
    cursor: not-allowed;
}

/* ─── Category collapse — grid-template-rows transition ──────── */
.category-body {
    display: grid;
    grid-template-rows: 1fr;
    transition: grid-template-rows 260ms ease;
}

.category[data-collapsed="true"] .category-body {
    grid-template-rows: 0fr;
}

/* A widget's body collapses on the same terms: it sits in the same
   .category-body wrapper, so it needs the same two properties for the grid row
   to shrink past its content. */
.category-body > .bookmarks-list,
.category-body > .dashboard-widget-body {
    min-height: 0;          /* required: lets the grid row shrink below content height */
    overflow: hidden;
    opacity: 1;
    transition: opacity 200ms ease;
}

.category:not([data-collapsed="true"]) .category-body > .bookmarks-list {
    overflow-x: visible;
    /*
     * Room for the cursor's bloom, taken back again.
     *
     * The list clips: it is `overflow: hidden` for the collapse animation, and
     * a horizontal `visible` beside a vertical `hidden` computes to `auto`, so
     * both axes clip at the padding box. The keyboard row's glow (--bloom,
     * 12px) was cut off square at the end of the row -- a pale block with a
     * hard edge where a soft halo should be. The padding moves that clip out
     * by the blur; the negative margin puts the box back where it was, so
     * nothing on the page moves and the scroll width is unchanged.
     */
    padding: 6px 12px;
    margin: -6px -12px;
    /* The padding would otherwise eat 24px of row width: the box grows by
       exactly what the margins pulled back, so a row is the width it was. */
    width: calc(100% + 24px);
}

.category[data-collapsed="true"] .category-body > .bookmarks-list,
.category[data-collapsed="true"] .category-body > .dashboard-widget-body {
    opacity: 0;
}

/* The height a widget body reserves is there so the packed layout does not
   shift when the figures arrive — a question that does not exist while the
   block is collapsed, and 3.5rem of reserved space is exactly what would keep a
   collapsed widget from closing. Scoped tightly enough to beat the rule that
   sets it, which is later in the file and equally specific. */
.dashboard-widget[data-collapsed="true"] .category-body > .dashboard-widget-body {
    min-height: 0;
    padding-top: 0;
    padding-bottom: 0;
    border-width: 0;
}

body.no-animations .category-body {
    transition: none !important;
}
body.no-animations .category-body > .bookmarks-list,
body.no-animations .category-body > .dashboard-widget-body {
    transition: none !important;
}

.category-title {
    cursor: pointer;
    user-select: none;
}

.category-title--renaming .category-title-label::before {
    content: none;
}

.category-chevron {
    display: inline-block;
    margin-left: 0.4em;
    font-size: 0.7em;
    opacity: 0.35;
    transition: transform 200ms ease, opacity 150ms ease;
    transform: rotate(0deg);
    vertical-align: 0.05em;
    pointer-events: none;
}

.category-chevron::after {
    content: '▾';
}

.category-title:hover {
    background: color-mix(in srgb, var(--hover-bg) 65%, transparent);
    border-radius: var(--layout-radius-sm, 8px);
    padding-left: var(--space-1, 0.25rem);
    margin-left: -0.25rem;
}

.category-title:hover .category-chevron {
    opacity: 0.7;
}

.category[data-collapsed="true"] .category-chevron {
    transform: rotate(-90deg);
}

body.no-animations .category-chevron {
    transition: none;
}

.smart-collection-title {
    color: var(--accent-primary);
}

.smart-collection-title .category-chevron {
    color: var(--accent-primary);
}

.smart-collection-why-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-left: 0.35em;
    padding: 0;
    width: 1.15em;
    height: 1.15em;
    border: none;
    border-radius: 50%;
    background: color-mix(in srgb, var(--accent-primary) 16%, var(--background-primary));
    color: var(--accent-primary);
    font-size: 0.62em;
    font-weight: var(--font-weight-bold);
    font-family: var(--font-family-main);
    line-height: 1;
    cursor: help;
    vertical-align: 0.08em;
    opacity: 0.82;
    pointer-events: auto;
    flex-shrink: 0;
}

.smart-collection-why-btn:hover,
.smart-collection-why-btn:focus-visible {
    opacity: 1;
    background: color-mix(in srgb, var(--accent-primary) 16%, var(--background-primary));
    outline: none;
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-primary) 22%, transparent);
}

.smart-collection-why-popover {
    position: fixed;
    z-index: 200000;
    margin: 0;
    padding: var(--space-2, 0.5rem) var(--space-3, 0.65rem);
    max-width: min(22rem, calc(100vw - 1rem));
    border-radius: var(--radius-4, 6px);
    border: 1px solid color-mix(in srgb, var(--accent-primary) 40%, var(--border-primary));
    background: var(--background-secondary, #141414);
    color: var(--text-secondary);
    font-family: var(--font-family-main);
    font-size: var(--font-size-controls);
    font-weight: var(--font-weight-normal);
    line-height: 1.45;
    letter-spacing: normal;
    text-transform: none;
    box-shadow:
        0 10px 28px rgba(0, 0, 0, 0.55),
        0 0 0 1px rgba(0, 0, 0, 0.35);
    pointer-events: none;
}

.smart-collection-why-popover[hidden] {
    display: none !important;
}

.smart-collection-why-btn[data-tooltip]::after {
    content: none !important;
    display: none !important;
}

.smart-collection-empty {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.45rem);
    margin: var(--space-1-5, 0.35rem) 0 var(--space-hairline, 0.15rem);
    padding: var(--space-2, 0.55rem) var(--space-3, 0.75rem);
    font-size: var(--font-size-controls);
    font-style: normal;
    font-weight: var(--font-weight-semibold);
    line-height: 1.45;
    color: var(--text-secondary);
    opacity: 1;
    border: 1px dashed color-mix(in srgb, var(--border-primary) 80%, transparent);
    border-radius: var(--layout-radius-sm, 8px);
    background: color-mix(in srgb, var(--background-secondary) 50%, transparent);
}

.smart-collection-empty::before {
    content: '○';
    flex-shrink: 0;
    font-size: 0.72em;
    font-weight: var(--font-weight-bold);
    color: var(--text-tertiary);
    opacity: 0.85;
}

.category-rename-input {
    background: transparent;
    border: 1px solid var(--border-primary);
    border-radius: var(--layout-radius-sm, 8px);
    color: inherit;
    font: inherit;
    font-size: inherit;
    font-weight: inherit;
    letter-spacing: inherit;
    padding: 0 0.25em;
    margin: 0;
    width: 12ch;
    min-width: 4ch;
    max-width: 28ch;
    outline: none;
    opacity: 1;
}

.category-rename-input:focus {
    border-color: var(--accent-primary);
}

/* Bookmarks */
.bookmarks-list {
    display: grid;
    grid-template-columns: var(--bookmark-lead-col, minmax(1.25rem, max-content)) minmax(0, 1fr) max-content;
    row-gap: var(--space-2, 0.5rem);
    min-width: 0;
    min-height: 2rem;
    width: 100%;
    --bookmark-row-start-pad: 6px;
    --bookmark-row-end-pad: 3px;
    --bookmark-status-col: 3.35em;
}

.bookmarks-list[data-show-ping="true"] {
    grid-template-columns:
        var(--bookmark-lead-col, minmax(1.25rem, max-content))
        minmax(0, 1fr)
        var(--bookmark-status-col, 3.35em)
        max-content;
}

.bookmarks-list > * {
    grid-column: 1 / -1;
}

body.bookmark-dragging .bookmarks-list.reorder-container:not([data-smart-collection="true"]):not(:has(.bookmark-link)) {
    min-height: 3rem;
    position: relative;
}

body.bookmark-dragging .bookmarks-list.reorder-container:not([data-smart-collection="true"]):not(:has(.bookmark-link))::after {
    content: '';
    position: absolute;
    inset: 0;
    border: 1px dashed var(--text-secondary);
    border-radius: var(--layout-radius-sm, 8px);
    opacity: 0.45;
    pointer-events: none;
    transform-origin: top center;
    animation: placeholderIn 220ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}

/* Subtle highlight on all reorder containers while dragging */
body.bookmark-dragging .bookmarks-list.reorder-container:not([data-smart-collection="true"]) {
    border-radius: var(--layout-radius-sm, 8px);
    outline: 1px dashed color-mix(in srgb, var(--text-secondary) 35%, transparent);
    outline-offset: 4px;
}

.bookmark-link {
    --bookmark-lead-col: minmax(1.25rem, max-content);
    display: grid;
    grid-template-columns: subgrid;
    grid-column: 1 / -1;
    grid-auto-rows: auto;
    align-items: center;
    --bookmark-row-gap: var(--space-2, 0.6rem);
    column-gap: var(--bookmark-row-gap);
    text-decoration: none;
    font-size: var(--font-size-bookmark);
    font-weight: var(--font-weight-semibold);
    transition: transform 140ms ease, opacity 120ms ease, color 140ms ease, background 140ms ease;
    position: relative;
    padding: var(--space-2, 0.5rem) var(--bookmark-row-end-pad, 3px) var(--space-2, 0.5rem) var(--bookmark-row-start-pad, 6px);
    color: var(--text-primary);
    max-width: 100%;
    min-height: 1.9rem;
}

.bookmark-link.animate-enter {
    opacity: 0;
    transform: translateY(6px);
    animation: bookmarkEnter 130ms ease-out forwards;
    animation-delay: calc(var(--item-index, 0) * 10ms);
}

/* Safety: status updates should never re-hide rows, even if animate-enter lingers. */
.bookmark-link.animate-enter.status-online,
.bookmark-link.animate-enter.status-offline,
.bookmark-link.animate-enter.status-checking {
    animation: none;
    opacity: 1;
    transform: none;
}

@keyframes bookmarkEnter {
    from {
        opacity: 0;
        transform: translateY(6px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* Single-row bookmark chrome (cards/list use multiline title; see overrides below) */
.dashboard-grid:not(.layout-cards):not(.layout-list) .bookmark-link:not(.bookmark-inline-editing) {
    white-space: nowrap;
}

/* Keep all row controls on one grid row (ping/shortcut/pin/edit) */
.bookmark-link:not(.bookmark-inline-editing) > * {
    grid-row: 1;
}

a.bookmark-open {
    grid-column: 2;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: var(--space-1, 0.3rem);
    color: inherit;
    text-decoration: none;
    overflow: hidden;
    outline: none;
}

a.bookmark-open:hover {
    color: inherit;
}

.bookmark-lead {
    position: relative;
    grid-column: 1;
    display: inline-flex;
    align-items: center;
    justify-content: flex-start;
    gap: var(--space-1-5, 0.35rem);
    min-width: 1.15em;
    min-height: 1.15em;
    cursor: grab;
}

.bookmarks-list[data-smart-collection="true"] .bookmark-lead {
    cursor: default;
}

.bookmark-reorder-handle {
    position: absolute;
    inset: 0;
    border-radius: var(--radius-2, 3px);
    cursor: grab;
    touch-action: none;
    background: transparent;
    opacity: 0;
    box-sizing: border-box;
}

.bookmark-link:hover .bookmark-reorder-handle {
    opacity: 0.55;
}

.bookmark-reorder-handle:active {
    cursor: grabbing;
}

.bookmarks-list[data-smart-collection="true"] .bookmark-reorder-handle {
    display: none;
}

.bookmark-link.bookmark-longpress-armed {
    user-select: none;
}

.bookmark-link.bookmark-longpress-armed .bookmark-open {
    user-select: none;
}

.category-title.category-title-longpress-armed {
    user-select: none;
}

/* Inline badge — shared base for pin, note, status */
.bookmark-superscript-badge {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    gap: 0.15em;
    font-size: var(--text-2xs, 0.6rem);
    font-weight: var(--font-weight-semibold);
    line-height: 1;
    padding: 0.18em 0.38em;
    border-radius: var(--radius-2, 3px);
    background: var(--background-secondary);
    color: var(--text-tertiary);
    border: 1px solid var(--border-secondary);
    white-space: nowrap;
    pointer-events: none;
    user-select: none;
    transition: opacity 120ms ease;
}

.bookmark-pin-badge svg {
    width: 0.7rem;
    height: 0.7rem;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
    display: inline-block;
}

.bookmark-pin-badge.is-empty {
    display: none;
}

.bookmark-note-badge svg {
    width: 0.7rem;
    height: 0.7rem;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
    display: inline-block;
}

.bookmark-note-badge.is-empty {
    display: none;
}

.bookmark-open-count {
    display: none;
}

/* "3 new" — what this page published since the row was last opened. It borrows
   the superscript badge's shape and adds only a hint of the accent: it is worth
   noticing on a scan and never worth reading first. */
.bookmark-fresh-badge {
    margin-left: 0.3em;
    color: var(--accent-primary);
    border-color: color-mix(in srgb, var(--accent-primary) 35%, var(--border-secondary));
    background: color-mix(in srgb, var(--accent-primary) 8%, var(--background-secondary));
}

.bookmark-fresh-badge.is-empty {
    display: none;
}

.bookmark-tags {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1, 0.2rem);
    margin-left: var(--space-1-5, 0.35rem);
}

.bookmark-tag-chip {
    font-size: var(--text-2xs, 0.68rem);
    line-height: 1;
    padding: 0.15em 0.45em;
    border-radius: var(--radius-pill, 999px);
    background: var(--background-secondary);
    color: var(--text-tertiary);
    border: 1px solid var(--border-secondary);
    white-space: nowrap;
    pointer-events: none;
    user-select: none;
}

.bookmark-shortcut {
    grid-column: 3;
    text-align: right;
    justify-self: end;
    white-space: nowrap;
}

.bookmarks-list[data-show-ping="true"] .bookmark-link > .status-text {
    grid-column: 3;
    grid-row: 1;
    justify-self: end;
    align-self: center;
}

.bookmarks-list[data-show-ping="true"] .bookmark-link > .bookmark-shortcut {
    grid-column: 4;
}

.bookmark-shortcut.is-empty {
    display: none;
}

.bookmark-icon-slot {
    width: 1.15em;
    height: 1.15em;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    /* The play mark hangs off this corner. No overflow clipping: the mark is
       meant to break the icon's edge, which is what makes it read as a badge
       on the icon rather than part of the picture. */
    position: relative;
}

.empty-state--category {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    padding: var(--space-1-5, 0.35rem) var(--space-2, 0.5rem);
    color: var(--text-tertiary);
    font-size: var(--font-size-controls);
}

.empty-state--category-btn {
    background: color-mix(in srgb, var(--accent-primary) 10%, transparent);
    border: 1px solid color-mix(in srgb, var(--accent-primary) 40%, var(--border-primary));
    border-radius: var(--layout-radius-sm, 8px);
    padding: var(--space-hairline, 0.15rem) var(--space-2, 0.45rem);
    font-size: var(--font-size-controls);
    color: var(--accent-primary);
    cursor: pointer;
    font-family: inherit;
    transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.empty-state--category-btn:hover {
    background: color-mix(in srgb, var(--accent-primary) 18%, transparent);
    border-color: var(--accent-primary);
    color: var(--accent-primary);
}

/* Per-category item cap: rows past the limit are hidden until "show more". */
.bookmark-link.is-overflow-hidden {
    display: none;
}

/* "+ N more" / "show less" toggle — a quiet, secondary control so it doesn't
   compete with the primary "+ bookmark" action. */
.category-show-more {
    display: inline-flex;
    align-items: center;
    margin-top: var(--space-hairline, 0.15rem);
    padding: var(--space-hairline, 0.1rem) var(--space-1-5, 0.35rem);
    background: transparent;
    border: none;
    border-radius: var(--layout-radius-sm, 8px);
    font-family: inherit;
    font-size: calc(var(--font-size-controls) * 0.92);
    color: var(--text-tertiary);
    cursor: pointer;
    transition: color 0.15s ease, background 0.15s ease;
}

.category-show-more:hover,
.category-show-more:focus-visible,
.category-show-more.keyboard-selected {
    color: var(--accent-primary);
    background: color-mix(in srgb, var(--accent-primary) 10%, transparent);
    outline: none;
}

/* Keyboard selection needs to read as "selected" even without :focus-visible,
   which browsers withhold after a click. */
.category-show-more.keyboard-selected {
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-primary) 45%, transparent);
}

/* Terminal layout: monospace style with > prefix */
.dashboard-grid.layout-terminal .empty-state--category {
    padding-left: var(--space-4, 1rem);
    position: relative;
}

.dashboard-grid.layout-terminal .empty-state--category-btn {
    font-family: "Source Code Pro", monospace;
    letter-spacing: 0.01em;
    background: none;
    border-color: color-mix(in srgb, var(--accent-primary) 45%, transparent);
    font-size: inherit;
}

.dashboard-grid.layout-terminal .empty-state--category::before {
    content: '> ';
    position: absolute;
    left: 0;
    opacity: 0.65;
    font-family: "Source Code Pro", monospace;
    pointer-events: none;
}

/* Launcher layout: tile-style add button */
.layout-launcher .empty-state--category {
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 88px;
    height: 80px;
    padding: var(--space-2, 0.5rem);
    gap: var(--space-1, 0.3rem);
}

.layout-launcher .empty-state--category-text {
    display: none;
}

.layout-launcher .empty-state--category-btn {
    width: 88px;
    height: 80px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-6, 10px);
    border-style: dashed;
    border-width: 1.5px;
    padding: 0;
    font-size: calc(var(--font-size-controls) * 0.85);
    opacity: 0.7;
    transition: opacity 0.15s ease, background 0.15s ease, border-color 0.15s ease;
}

.layout-launcher .empty-state--category-btn:hover {
    opacity: 1;
    background: color-mix(in srgb, var(--accent-primary) 15%, transparent);
    border-color: var(--accent-primary);
}

.bookmark-icon-letter {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--background-secondary, rgba(128, 128, 128, 0.15));
    color: var(--text-tertiary, currentColor);
    font-size: 0.6em;
    font-weight: 600;
    border-radius: var(--layout-radius-xs, 4px);
    text-transform: uppercase;
    line-height: 1;
    user-select: none;
    pointer-events: none;
    flex-shrink: 0;
}

.bookmark-link.bookmark-inline-editing {
    display: block;
    padding: var(--space-1-5, 0.35rem) 0;
    min-height: 0;
    min-width: 0;
}

/* Launcher tiles are narrow glass chips; inline edit needs a full-width panel row. */
.layout-launcher .bookmark-link.bookmark-inline-editing {
    width: auto;
    min-width: min(420px, 90vw);
    max-width: min(520px, 95vw);
    flex: 1 1 100%;
    align-items: stretch;
    padding: 0;
    background: transparent;
    border: none;
    box-shadow: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    transform: none;
}

.bookmark-link.bookmark-inline-editing.keyboard-selected {
    background: none;
}

/*
 * Inline edit focus: dim the chrome that is not being edited.
 *
 * Dimmed, not blurred. `filter: blur()` on the neighbouring rows gave each
 * column its own compositing layer, and WebKit paints those in an order that
 * ignores the z-index of unrelated elements — so a tall inline form opened from
 * Health had a blurred neighbouring column painted on top of it, eating every
 * click on its fields. `pointer-events: none` does not help: WebKit hit-tests
 * the composited layer regardless. The same class of bug was fixed the same way
 * in the check-mode popover, by taking the blur out.
 *
 * At 18% opacity the blur was doing almost nothing visually, which is what
 * makes this a straight removal rather than a trade.
 *
 * Do NOT use a body::before backdrop-filter overlay either — it stacks above
 * #dashboard-layout and frosts the form in Safari and Chrome.
 */
body.bookmark-inline-edit-active {
    --inline-edit-panel-bg: var(--background-primary);
    --inline-edit-field-bg: var(--background-secondary);
}

body.bookmark-inline-edit-active .dashboard-section.section-controls,
body.bookmark-inline-edit-active .dashboard-section.section-title,
body.bookmark-inline-edit-active .button-container,
body.bookmark-inline-edit-active #dashboard-layout .category-title,
body.bookmark-inline-edit-active #dashboard-layout .bookmark-link:not(.bookmark-inline-editing) {
    opacity: 0.18;
    pointer-events: none;
    transition: opacity 0.2s ease;
}

body.bookmark-inline-edit-active #dashboard-layout .bookmark-link:not(.bookmark-inline-editing) > * {
    /* No filter here: see above. The row is already at 18% opacity. */
    pointer-events: none;
}

/*
 * And no backdrop-filter on the cards while a form is open, for the same
 * reason and against the same bug.
 *
 * The blur that caused this originally was on the rows and is long gone. The
 * glass depth step puts one back at card level: on `glass`, every .category
 * and every widget body composites. Measured with a form open, five of those
 * cards overlapped it -- each its own layer, each at full opacity, which is
 * exactly the arrangement WebKit paints over an unrelated element regardless
 * of z-index and then hit-tests in front of it.
 *
 * Every card, not only the neighbours: the one being edited is behind the
 * form too, and the form draws its own opaque panel over all of them. What is
 * lost is the blur behind a card nobody can see for as long as the form is
 * up, which is the same trade the row blur was.
 */
body.bookmark-inline-edit-active #dashboard-layout .category,
body.bookmark-inline-edit-active #dashboard-layout .dashboard-widget-body {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}

body.bookmark-inline-edit-active #shortcut-search:not(.show) {
    opacity: 0.18;
    pointer-events: none;
    transition: opacity 0.2s ease;
}

body.bookmark-inline-edit-active .bookmark-link.bookmark-inline-editing {
    filter: none;
    opacity: 1;
    pointer-events: auto;
    position: relative;
    z-index: 20;
    isolation: isolate;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    touch-action: manipulation;
}

body.bookmark-inline-edit-active .bookmark-inline-form,
body.bookmark-inline-edit-active .bookmark-inline-form * {
    pointer-events: auto;
    -webkit-user-select: text;
    user-select: text;
}

body.bookmark-inline-edit-active .category .bookmarks-list,
body.bookmark-inline-edit-active .category-body > .bookmarks-list {
    overflow: visible;
    min-height: auto;
}

@media (prefers-reduced-motion: reduce) {
    body.bookmark-inline-edit-active .dashboard-section.section-controls,
    body.bookmark-inline-edit-active .dashboard-section.section-title,
    body.bookmark-inline-edit-active .button-container,
    body.bookmark-inline-edit-active #dashboard-layout .category-title,
    body.bookmark-inline-edit-active #dashboard-layout .bookmark-link:not(.bookmark-inline-editing),
    body.bookmark-inline-edit-active #shortcut-search:not(.show) {
        transition: none;
    }

    body.bookmark-inline-edit-active #dashboard-layout .bookmark-link:not(.bookmark-inline-editing) > * {
        filter: none;
    }
}

body.bookmark-inline-edit-active .bookmark-inline-form {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    filter: none;
    background-color: var(--inline-edit-panel-bg);
    background-image: none;
}

body.bookmark-inline-edit-active #app-modal.modal-overlay.show {
    z-index: 10050;
}

.bookmark-inline-form {
    width: 100%;
    min-width: min(420px, 90vw);
    max-width: none;
    display: flex;
    flex-direction: column;
    gap: var(--space-1-5, 0.35rem);
    padding: var(--space-3, 0.7rem) var(--space-3, 0.8rem);
    border-radius: var(--radius-5, 8px);
    /* Solid panel — never rgba modal (see --inline-edit-panel-bg). */
    background-color: var(--inline-edit-panel-bg);
    border: 2px solid var(--border-primary);
    box-shadow:
        0 0 0 1px color-mix(in srgb, var(--accent-primary) 22%, var(--border-primary)),
        0 12px 36px color-mix(in srgb, var(--text-primary) 18%, transparent);
    box-sizing: border-box;
    position: relative;
    z-index: 1;
    isolation: isolate;
    animation: inlineEditReveal 180ms ease-out;
}

.bookmark-inline-form .bookmark-inline-input,
.bookmark-inline-form .bookmark-inline-select,
.bookmark-inline-form .bookmark-inline-textarea,
.bookmark-inline-form .bookmark-inline-action-btn,
.bookmark-inline-form .bookmark-inline-icon-preview {
    background-color: var(--inline-edit-field-bg);
    pointer-events: auto;
    user-select: text;
    -webkit-user-select: text;
}

@keyframes inlineEditReveal {
    from {
        opacity: 0;
        transform: translateY(-4px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

@media (prefers-reduced-motion: reduce) {
    .bookmark-inline-form {
        animation: none;
    }
}

.bookmark-inline-field {
    display: flex;
    flex-direction: column;
    gap: var(--space-hairline, 0.15rem);
    min-width: 0;
}

/* ─── Flag toggles (pinned / status check / monitor) ─────────────────────────
   One horizontal row of pills instead of three stacked checkbox rows: the flags
   read as a single group, sit near the top where they are easy to find, and cost
   one line instead of three. */

.bookmark-inline-toggles {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1, 0.3rem);
}

/* The real checkbox stays focusable and reachable by screen readers; the pill
   label is the visible control. */
.bookmark-inline-toggle-input {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
}

.bookmark-inline-toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1, 0.3rem);
    padding: var(--space-1, 0.25rem) var(--space-2, 0.55rem);
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-pill, 999px);
    background-color: var(--inline-edit-field-bg);
    color: var(--text-secondary);
    font-size: var(--font-size-controls);
    line-height: 1.2;
    cursor: pointer;
    user-select: none;
    transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
}

.bookmark-inline-toggle svg {
    width: 0.85em;
    height: 0.85em;
    fill: currentColor;
    flex: 0 0 auto;
}

.bookmark-inline-toggle:hover {
    border-color: color-mix(in srgb, var(--accent-primary) 45%, var(--border-secondary));
    color: var(--text-primary);
}

.bookmark-inline-toggle-input:checked + .bookmark-inline-toggle {
    border-color: var(--accent-primary);
    background-color: color-mix(in srgb, var(--accent-primary) 14%, var(--inline-edit-field-bg));
    color: var(--text-primary);
    font-weight: var(--font-weight-semibold);
}

/* Keyboard focus must stay visible even though the input itself is off-screen. */
.bookmark-inline-toggle-input:focus-visible + .bookmark-inline-toggle {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
}

/* ─── Availability check mode ────────────────────────────────────────────────
   A segmented control, not two checkboxes: Monitor is a superset of Periodic, so
   only one can be true at a time and the UI should make that structural rather
   than rely on the user knowing it. */

.bookmark-inline-checkmode {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1, 0.25rem);
}

.bookmark-inline-checkmode-input {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
}

.bookmark-inline-checkmode-option {
    padding: var(--space-1, 0.25rem) var(--space-2, 0.6rem);
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-pill, 999px);
    background-color: var(--inline-edit-field-bg);
    color: var(--text-secondary);
    font-size: var(--font-size-controls);
    line-height: 1.2;
    cursor: pointer;
    user-select: none;
    transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
}

.bookmark-inline-checkmode-option:hover {
    border-color: color-mix(in srgb, var(--accent-primary) 45%, var(--border-secondary));
    color: var(--text-primary);
}

.bookmark-inline-checkmode-input:checked + .bookmark-inline-checkmode-option {
    border-color: var(--accent-primary);
    background-color: color-mix(in srgb, var(--accent-primary) 14%, var(--inline-edit-field-bg));
    color: var(--text-primary);
    font-weight: var(--font-weight-semibold);
}

.bookmark-inline-checkmode-input:focus-visible + .bookmark-inline-checkmode-option {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
}

.bookmark-inline-checkmode-info {
    width: 1.15rem;
    height: 1.15rem;
    padding: 0;
    border: 1px solid var(--border-secondary);
    border-radius: 50%;
    background: transparent;
    color: var(--text-tertiary);
    font-size: var(--text-xs, 0.7rem);
    font-style: italic;
    line-height: 1;
    cursor: pointer;
    flex: 0 0 auto;
}

.bookmark-inline-checkmode-info:hover {
    border-color: var(--accent-primary);
    color: var(--text-primary);
}

/* Explainer modal */
.check-mode-explain-row + .check-mode-explain-row {
    margin-top: var(--space-4, 0.9rem);
}

.check-mode-explain-row h4 {
    margin: 0 0 var(--space-1, 0.2rem);
    font-size: var(--text-base, 0.95rem);
    color: var(--text-primary);
}

.check-mode-explain-row p {
    margin: 0;
    color: var(--text-secondary);
    line-height: 1.5;
}

@media (prefers-reduced-motion: reduce) {
    .bookmark-inline-checkmode-option {
        transition: none;
    }
}

/* Overrides the shared .bookmark-inline-select width:100% (defined later in this
   file) so the interval sits beside the pills instead of claiming its own line —
   the extra specificity is the point, not an accident. */
.bookmark-inline-toggles .bookmark-inline-select.bookmark-inline-toggle-select,
.bookmark-inline-checkmode .bookmark-inline-select.bookmark-inline-toggle-select {
    flex: 0 1 auto;
    width: auto;
    min-width: 0;
    max-width: 100%;
    padding: var(--space-1, 0.2rem) var(--space-1-5, 0.4rem);
    font-size: var(--font-size-controls);
}

@media (prefers-reduced-motion: reduce) {
    .bookmark-inline-toggle {
        transition: none;
    }
}

.bookmark-inline-label {
    font-size: var(--font-size-controls);
    color: var(--text-tertiary);
    font-weight: var(--font-weight-semibold);
}

.bookmark-inline-input,
.bookmark-inline-select {
    width: 100%;
    font-family: inherit;
    font-size: var(--font-size-controls);
    padding: var(--space-1, 0.25rem) var(--space-1-5, 0.4rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--layout-radius-sm, 8px);
    background: var(--background-secondary);
    color: var(--text-primary);
    outline: none;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
    -webkit-appearance: none;
    appearance: none;
    pointer-events: auto;
    touch-action: manipulation;
    -webkit-user-select: text;
    user-select: text;
}

.bookmark-inline-input:focus,
.bookmark-inline-select:focus {
    border-color: var(--accent-primary);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-primary) 20%, transparent);
}

.bookmark-inline-input.input-error,
.bookmark-inline-input.field-conflict {
    border-color: var(--accent-error);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-error) 20%, transparent);
}

.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;
}

.bookmark-inline-action-btn:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

.bookmark-inline-action-btn.bookmark-inline-save--invalid,
.bookmark-inline-action-btn[aria-disabled="true"] {
    opacity: 0.4;
    cursor: not-allowed;
}

.bookmark-inline-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1-5, 0.35rem);
    margin-top: var(--space-1, 0.25rem);
    align-items: center;
    position: relative;
    z-index: 5;
}

.bookmark-inline-actions .bookmark-inline-action-btn {
    position: relative;
    z-index: 2;
}

.bookmark-inline-hint {
    flex: 1 1 auto;
    min-width: 0;
    margin-left: auto;
    font-size: var(--text-xs, 0.7rem);
    color: var(--text-tertiary);
    white-space: nowrap;
    pointer-events: none;
    text-align: right;
}

.bookmark-inline-hint kbd {
    font-family: var(--font-family-main);
    font-size: var(--text-2xs, 0.68rem);
    background: var(--background-secondary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-2, 3px);
    padding: var(--space-hairline, 0.05rem) var(--space-1, 0.3rem);
    color: var(--text-secondary);
}

.bookmark-inline-conflict {
    display: block;
    font-size: var(--text-xs, 0.72rem);
    color: var(--accent-error);
    margin-top: var(--space-1, 0.2rem);
}

.bookmark-inline-conflict[hidden] {
    display: none;
}

/* ─── Inline create (new page / new category from the form's dropdowns) ───────
   The row takes the select's place inside the same field, so choosing "New …"
   never reflows the form around it. The name input takes the leftover width and
   the two buttons keep their intrinsic size. */

.bookmark-inline-create {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1-5, 0.35rem);
}

.bookmark-inline-create[hidden] {
    display: none;
}

.bookmark-inline-create .bookmark-inline-create-input {
    flex: 1 1 8rem;
    width: auto;
    min-width: 0;
}

.bookmark-inline-create .bookmark-inline-create-btn {
    flex: 0 0 auto;
}

/* The error spans the whole row rather than squeezing in beside the buttons. */
.bookmark-inline-create .bookmark-inline-conflict {
    flex: 1 0 100%;
    margin-top: 0;
}

/* Sets the create entry apart from the real pages/categories below it. */
.bookmark-inline-new-option {
    font-weight: var(--font-weight-semibold);
}

/* ─── "+ category" — the grid's own add affordance ───────────────────────────
   Sits after the last column rather than inside one: it is not a category, and
   the drag-reorder instances would treat it as one if it were. Dashed and muted
   so it reads as an invitation next to real category headers, not as content.
   `c` does the same thing without it, which is why it may be hidden entirely on
   touch. */

/* Sized and coloured to match .category-sort-btn beside it: this is one more
   control in the same trailing group, not a separate affordance. Squarer than
   the A–Z/Rec chips because it holds a single glyph. */
.category-add-inline-btn {
    font: inherit;
    font-size: 0.68em;
    font-weight: var(--font-weight-semibold);
    line-height: 1.2;
    min-height: 1.45em;
    min-width: 1.45em;
    padding: 0.15em 0.3em;
    margin-left: 0.25em;
    border: 1px solid color-mix(in srgb, var(--text-tertiary) 28%, transparent);
    border-radius: var(--radius-2, 3px);
    background: color-mix(in srgb, var(--background-secondary, transparent) 55%, transparent);
    color: var(--text-tertiary);
    cursor: pointer;
    opacity: 0.75;
    transition: opacity 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}

.category-add-inline-btn:hover,
.category-add-inline-btn:focus-visible {
    opacity: 1;
    color: var(--text-secondary);
    border-color: color-mix(in srgb, var(--text-secondary) 35%, transparent);
    outline: none;
}

.category-add-inline-btn[hidden] {
    display: none;
}

/* The name row is hosted by the grid rather than the header the "+" sits in: a
   text input inside a one-line header would overflow the column. It spans the
   grid so it is never squeezed into a single column's width, and only exists
   while naming, so the row it adds is not permanent. */
.bookmark-inline-create.category-add-create {
    grid-column: 1 / -1;
    max-width: 22rem;
    margin-top: var(--space-3, 0.75rem);
}

.bookmark-inline-icon-actions {
    display: flex;
    align-items: center;
    gap: var(--space-1-5, 0.35rem);
    margin-top: var(--space-1, 0.3rem);
}

.bookmark-inline-icon-preview {
    width: 2rem;
    height: 2rem;
    border: 1px solid var(--border-primary);
    border-radius: var(--layout-radius-sm, 8px);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--background-primary);
    color: var(--text-tertiary);
    margin-top: var(--space-1, 0.25rem);
    overflow: hidden;
}

.bookmark-inline-icon-preview img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

.bookmark-inline-icon-preview span {
    font-size: var(--text-2xs, 0.65rem);
    line-height: 1;
}

.bookmark-inline-icon-state {
    font-size: var(--font-size-controls);
    color: var(--text-tertiary);
}

.bookmark-inline-action-btn,
.bookmark-inline-create-btn {
    border: 1px solid var(--border-primary);
    background: var(--background-primary);
    color: var(--text-primary);
    font-size: var(--font-size-controls);
    padding: var(--space-1, 0.3rem) var(--space-2, 0.55rem);
    border-radius: var(--layout-radius-sm, 8px);
    cursor: pointer;
}

.bookmark-inline-save,
.bookmark-inline-create-ok {
    border-color: var(--text-secondary);
}

.bookmark-inline-create-btn:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

.bookmark-inline-delete {
    color: var(--text-secondary);
}

@keyframes bookmarkMoveOut {
    0%   { opacity: 1; transform: translateX(0); }
    60%  { opacity: 0.3; transform: translateX(18px); }
    100% { opacity: 0; transform: translateX(32px); max-height: 0; padding: 0; margin: 0; }
}

.bookmark-move-out {
    animation: bookmarkMoveOut 0.32s ease-in forwards;
    overflow: hidden;
    pointer-events: none;
}

.bookmark-inline-delete:hover {
    border-color: var(--text-primary);
    color: var(--text-primary);
}

.bookmarks-list.reorder-container .bookmark-link.is-draggable {
    opacity: 0.45;
    transform: translateX(2px) scale(0.985);
}

.bookmarks-list.reorder-container .bookmark-drop-placeholder {
    height: 1.6rem;
    border: 1px dashed var(--text-secondary);
    border-radius: var(--layout-radius-sm, 8px);
    opacity: 0.65;
    background: color-mix(in srgb, var(--background-primary) 85%, var(--text-secondary) 15%);
    transform-origin: top center;
    animation: placeholderIn 220ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}

@keyframes placeholderIn {
    from {
        opacity: 0;
        transform: scaleY(0.3);
    }
    to {
        opacity: 0.65;
        transform: scaleY(1);
    }
}

.bookmarks-list.reorder-container .bookmark-link.bookmark-move-in {
    animation: bookmarkMoveIn 180ms ease-out;
}

@keyframes bookmarkMoveIn {
    0% {
        opacity: 0.25;
        transform: translateX(8px) scale(0.97);
    }
    100% {
        opacity: 1;
        transform: translateX(0) scale(1);
    }
}

body.no-animations .bookmarks-list.reorder-container .bookmark-link,
body.no-animations .bookmarks-list.reorder-container .bookmark-link.bookmark-move-in,
body.no-animations .bookmarks-list.reorder-container .bookmark-drop-placeholder,
body.no-animations .bookmarks-list.reorder-container::after {
    transition: none;
    animation: none;
}

.bookmark-icon {
    width: 100%;
    height: 100%;
    object-fit: contain;
    flex-shrink: 0;
    -webkit-user-drag: none;
    user-drag: none;
    pointer-events: none;
}

.bookmark-link .bookmark-icon {
    display: block;
}

.category-title .bookmark-icon {
    width: 0.85em;
    height: 0.85em;
    vertical-align: -0.05em;
}

@keyframes smartRecentPromote {
    0% {
        transform: translateY(10px);
        opacity: 0.65;
        box-shadow: 0 0 0 1px var(--accent-success);
    }
    100% {
        transform: translateY(0);
        opacity: 1;
        box-shadow: 0 0 0 0 transparent;
    }
}

.bookmark-text {
    flex: 1;
    min-width: 0; /* Allow text to shrink */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: 0.88em;
    line-height: 1.2;
}

/*
 * The link preview card.
 *
 * Three bands in a fixed order — what it is, what it says, what I know about it
 * — each one absent rather than blank when it has nothing to carry. No card-level
 * max-height: the blocks clamp themselves, so nothing disappears without the
 * card being visibly shorter for it.
 */
.bookmark-preview-card {
    position: fixed;
    z-index: 2000;
    display: none;
    width: min(360px, calc(100vw - 24px));
    border-radius: var(--radius-7, 12px);
    border: 1px solid var(--border-primary);
    background: var(--background-secondary);
    box-shadow: 0 10px 28px rgba(0, 0, 0, 0.28);
    /* Peek is something to read, not something to reach: it takes no pointer
       events, so it never has to move out of the way of its own cursor. */
    pointer-events: none;
    overflow: hidden;
}

/* `display: flex` and `display: grid` both beat the hidden attribute, and the
   card's bands are laid out with both — so a band that says it is absent has to
   be made absent here, or it draws as an empty stripe. */
.bookmark-preview-card [hidden] {
    display: none !important;
}

.bookmark-preview-card.is-visible {
    display: block;
}

.bookmark-preview-card.is-pinned {
    pointer-events: auto;
    border-color: var(--text-secondary);
}

.bookmark-preview-card:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
}

.bookmark-preview-card-head {
    display: flex;
    gap: var(--space-2, 0.55rem);
    align-items: flex-start;
    padding: var(--space-3, 0.7rem) var(--space-3, 0.8rem) var(--space-2, 0.5rem);
}

.bookmark-preview-card-fav {
    flex: none;
    width: 17px;
    height: 17px;
    margin-top: 2px;
    border-radius: var(--radius-3, 4px);
    overflow: hidden;
    background: var(--background-primary);
}

.bookmark-preview-card-fav img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

.bookmark-preview-card-headtext {
    flex: 1;
    min-width: 0;
}

.bookmark-preview-card-title {
    font-size: var(--font-size-controls);
    font-weight: var(--font-weight-bold);
    color: var(--text-primary);
    line-height: 1.25;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
}

/* One address, not two: the old card printed the full URL in mono and then its
   hostname on the next line, and neither could be clicked or copied. */
.bookmark-preview-card-domain {
    margin-top: 2px;
    font-family: var(--font-family-mono, monospace);
    font-size: calc(var(--font-size-controls) * 0.8);
    color: var(--text-tertiary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The byline sits with the address, not in the facts list below: publisher,
   author and date read as one sentence, and three labelled rows would make a
   paragraph out of a glance. Set apart from the address by weight, not colour,
   so the mono address stays the anchor. */
.bookmark-preview-card-byline {
    margin-top: 3px;
    font-size: calc(var(--font-size-controls) * 0.78);
    color: var(--text-secondary);
    line-height: 1.35;
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

/* The provider's player, in a sandboxed frame. 16:9 is what video oEmbed
   returns almost without exception, and letting the frame size itself would
   collapse it to nothing -- srcdoc content has no intrinsic height. */
.bookmark-preview-card-embed {
    margin-top: 10px;
    border-radius: var(--radius-md, 8px);
    overflow: hidden;
    background: #000;
    aspect-ratio: 16 / 9;
}

.bookmark-preview-card-embed-frame {
    display: block;
    width: 100%;
    height: 100%;
    border: 0;
}

/*
 * The poster: this card's own picture, with a play button over it.
 *
 * It re-enables pointer events for itself, because the card around it takes
 * none while it is only being read -- which is what lets a peek card carry a
 * button at all. Nothing here reaches the provider: the picture is the
 * preview image already stored, and the frame is built when this is pressed.
 */
.bookmark-preview-card-poster {
    position: relative;
    display: block;
    width: 100%;
    height: 100%;
    padding: 0;
    border: 0;
    background: #000;
    cursor: pointer;
    pointer-events: auto;
}

.bookmark-preview-card-poster-image {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* Dimmed so the play mark reads against a bright frame from the video. */
    opacity: 0.72;
    transition: opacity 120ms ease;
}

.bookmark-preview-card-poster:hover .bookmark-preview-card-poster-image,
.bookmark-preview-card-poster:focus-visible .bookmark-preview-card-poster-image {
    opacity: 0.9;
}

.bookmark-preview-card-poster-play {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 52px;
    height: 52px;
    border-radius: 50%;
    border: 1px solid rgba(255, 255, 255, 0.55);
    background: rgba(0, 0, 0, 0.55);
    color: #fff;
}

.bookmark-preview-card-poster-play svg {
    width: 26px;
    height: 26px;
    fill: currentColor;
}

.bookmark-preview-card-poster:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: -2px;
}

/* The player's own way out. Escape reaches the card only while the page has
   the keyboard, and a click inside the frame hands that to the provider --
   so the player carries a close of its own, over its top corner. */
.bookmark-preview-card-embed {
    position: relative;
}

.bookmark-preview-card-embed-close {
    position: absolute;
    top: 6px;
    right: 6px;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    padding: 0;
    border-radius: 50%;
    border: 1px solid rgba(255, 255, 255, 0.45);
    background: rgba(0, 0, 0, 0.6);
    color: #fff;
    font-size: 13px;
    line-height: 1;
    cursor: pointer;
    pointer-events: auto;
}

.bookmark-preview-card-embed-close:hover,
.bookmark-preview-card-embed-close:focus-visible {
    background: rgba(0, 0, 0, 0.85);
    border-color: #fff;
}

/* A row that plays something.
 *
 * The mark takes its colour from the element, so no fill or stroke can go
 * missing under a theme that paints icons its own way. It has two homes.
 *
 * On the favicon's corner, the way a video service draws play over a
 * thumbnail. It is a disc rather than a bare triangle because it sits on a
 * picture it does not choose, and it costs the row no width at all.
 */
.bookmark-video-badge.is-on-icon {
    position: absolute;
    /* In percentages of the icon, never in em: a browser's minimum font size
       (Safari has one) raises small text, and a disc sized in the text's em
       grew with it until it covered the whole favicon. */
    right: -12%;
    bottom: -12%;
    width: 50%;
    height: 50%;
    border-radius: 50%;
    /* The disc reads on a light favicon and a dark one alike, so it brings its
       own ground rather than borrowing the row's. */
    background: var(--accent-primary);
    color: var(--background-primary);
    /* A hairline in the page's own colour keeps the disc off the picture's
       edge, so the two never smear into one shape. */
    box-shadow: 0 0 0 1px var(--background-primary);
    pointer-events: none;
    user-select: none;
}

/* The triangle: a shape in the disc's text colour, in its optical middle --
   a hair right of centre, because its weight is on the left. */
.bookmark-video-badge.is-on-icon::before {
    content: '';
    position: absolute;
    left: 36%;
    top: 27%;
    width: 38%;
    height: 46%;
    background: currentColor;
    clip-path: polygon(0 0, 100% 50%, 0 100%);
}

/* The launcher draws icons at 32-64px, where half the tile would be a
   second icon. A third of it still reads as a corner mark. */
.layout-launcher .bookmark-video-badge.is-on-icon {
    width: 34%;
    height: 34%;
    right: -6%;
    bottom: -6%;
}

.bookmark-video-badge.is-bare {
    flex-shrink: 0;
    margin-left: 0.3em;
    color: var(--accent-primary);
    font-size: 0.72em;
    line-height: 1.05;
    pointer-events: none;
    user-select: none;
}

/* "Is this link still alive" is the question the rest of the app exists to
   answer, so it is the first thing on the card. */
.bookmark-preview-card-pill {
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 4px 7px;
    border-radius: var(--radius-pill, 999px);
    border: 1px solid var(--border-secondary);
    background: rgba(255, 255, 255, 0.03);
    color: var(--text-secondary);
    font-size: calc(var(--font-size-controls) * 0.72);
    font-weight: var(--font-weight-semibold);
    white-space: nowrap;
}

.bookmark-preview-card-pill::before {
    content: '';
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: currentColor;
}

.bookmark-preview-card-pill.is-ok {
    color: var(--accent-success, #10B981);
    border-color: color-mix(in srgb, var(--accent-success, #10B981) 40%, transparent);
    background: color-mix(in srgb, var(--accent-success, #10B981) 12%, transparent);
}

.bookmark-preview-card-pill.is-warn {
    color: var(--accent-warning, #F59E0B);
    border-color: color-mix(in srgb, var(--accent-warning, #F59E0B) 40%, transparent);
    background: color-mix(in srgb, var(--accent-warning, #F59E0B) 12%, transparent);
}

.bookmark-preview-card-pill.is-bad {
    color: var(--accent-error, #EF4444);
    border-color: color-mix(in srgb, var(--accent-error, #EF4444) 40%, transparent);
    background: color-mix(in srgb, var(--accent-error, #EF4444) 12%, transparent);
}

.bookmark-preview-card-image-wrap {
    width: 100%;
    aspect-ratio: 16 / 9;
    max-height: 158px;
    border-top: 1px solid var(--border-secondary);
    border-bottom: 1px solid var(--border-secondary);
    background: var(--background-primary);
}

.bookmark-preview-card-image {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.bookmark-preview-card-description {
    padding: var(--space-2, 0.55rem) var(--space-3, 0.8rem) 0;
    font-size: calc(var(--font-size-controls) * 0.92);
    color: var(--text-secondary);
    line-height: 1.42;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    overflow: hidden;
}

.bookmark-preview-card-empty {
    margin: 0;
    padding: var(--space-2, 0.55rem) var(--space-3, 0.8rem) 0;
    font-size: calc(var(--font-size-controls) * 0.88);
    font-style: italic;
    color: var(--text-tertiary);
}

.bookmark-preview-card-facts {
    margin: var(--space-3, 0.7rem) 0 0;
    padding: var(--space-2, 0.5rem) var(--space-3, 0.8rem) var(--space-2, 0.6rem);
    border-top: 1px solid var(--border-secondary);
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 3px 12px;
    font-size: calc(var(--font-size-controls) * 0.86);
}

.bookmark-preview-card-facts dt {
    font-family: var(--font-family-mono, monospace);
    font-size: calc(var(--font-size-controls) * 0.74);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    white-space: nowrap;
    line-height: 1.6;
}

.bookmark-preview-card-facts dd {
    margin: 0;
    color: var(--text-secondary);
    line-height: 1.6;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

.bookmark-preview-card-facts dd b {
    color: var(--text-primary);
    font-weight: var(--font-weight-semibold);
}

.bookmark-preview-card-facts dd.is-good b { color: var(--accent-success, #10B981); }
.bookmark-preview-card-facts dd.is-warn b { color: var(--accent-warning, #F59E0B); }
.bookmark-preview-card-facts dd.is-bad b { color: var(--accent-error, #EF4444); }

.bookmark-preview-card-facts dd kbd {
    font-family: var(--font-family-mono, monospace);
    font-size: calc(var(--font-size-controls) * 0.78);
    border: 1px solid var(--border-primary);
    border-bottom-width: 2px;
    border-radius: var(--radius-3, 4px);
    padding: 1px 5px;
    color: var(--text-primary);
}

.bookmark-preview-card-muted {
    color: var(--text-tertiary);
}

/* Only a pinned card has actions, so there is nothing on a peek to aim at. */
.bookmark-preview-card-foot {
    display: flex;
    gap: 4px;
    align-items: center;
    padding: var(--space-2, 0.45rem) var(--space-2, 0.55rem);
    border-top: 1px solid var(--border-secondary);
    background: rgba(0, 0, 0, 0.18);
}

.bookmark-preview-card-action {
    border: 1px solid transparent;
    border-radius: var(--radius-4, 6px);
    padding: 5px 8px;
    background: none;
    color: var(--text-secondary);
    font-size: calc(var(--font-size-controls) * 0.8);
    cursor: pointer;
}

.bookmark-preview-card-action:hover,
.bookmark-preview-card-action:focus-visible {
    color: var(--text-primary);
    border-color: var(--border-secondary);
    background: rgba(255, 255, 255, 0.04);
    outline: none;
}

.bookmark-preview-card-action:disabled {
    opacity: 0.65;
    cursor: wait;
}

.bookmark-preview-card-esc {
    margin-left: auto;
    font-family: var(--font-family-mono, monospace);
    font-size: calc(var(--font-size-controls) * 0.72);
    color: var(--text-tertiary);
}

/* Bookmark open flash animation */
@keyframes bookmarkFlash {
    0%   { opacity: 0.55; }
    100% { opacity: 0; }
}

.bookmark-link {
    position: relative;
}

/* Copy-URL row flash */
@keyframes bookmarkCopyFlash {
    0%   { opacity: 0.45; }
    100% { opacity: 0; }
}

.bookmark-link.bookmark-copy-flash::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: var(--color-success, #4caf7d);
    opacity: 0;
    pointer-events: none;
    animation: bookmarkCopyFlash 500ms ease-out forwards;
}

    body.no-animations .bookmark-link.bookmark-copy-flash::after {
    animation: none;
}

/* Launcher scale-pulse on open */
@keyframes bookmarkPulse {
    0%   { transform: scale(1); }
    30%  { transform: scale(0.95); }
    70%  { transform: scale(1.05); }
    100% { transform: scale(1); }
}

.layout-launcher .bookmark-link.bookmark-pulse {
    animation: bookmarkPulse 260ms ease-out forwards;
}

body.no-animations .layout-launcher .bookmark-link.bookmark-pulse {
    animation: none;
}

/* Keyboard navigation — row highlight lives in theme.css */
.bookmark-link.keyboard-selected {
    position: relative;
}

.layout-launcher .bookmark-link.keyboard-selected,
.layout-launcher .bookmark-link:focus-within {
    outline: none;
    box-shadow: none;
}

.bookmark-shortcut {
    flex-shrink: 0;
    font-family: var(--font-family-mono, monospace);
    font-size: var(--text-2xs, 0.68rem);
    font-weight: var(--font-weight-normal);
    color: var(--text-tertiary);
    background: color-mix(in srgb, var(--background-secondary) 80%, transparent);
    border: 1px solid color-mix(in srgb, var(--border-primary) 65%, transparent);
    border-radius: var(--layout-radius-sm, 8px);
    padding: 0.1em 0.4em;
    line-height: 1.6;
    letter-spacing: 0.03em;
    white-space: nowrap;
    opacity: 1;
}

/* Loading */
.loading {
    text-align: center;
    font-size: var(--font-size-category);
    opacity: 0.7;
    grid-column: 1 / -1;
}

/* Visibility Controls */
body[data-show-title="false"] .title-wrapper {
    display: none;
}

/* Hide config button when showConfigButton is false */
body[data-show-config-button="false"] .config-link {
    display: none;
}

/* The inbox, on its own switch. Its host is filled by dashboard-inbox.js, so
   it is hidden rather than left out. The pages button is one of the actions
   now and hides with them, further up. */
body[data-show-inbox-button="false"] #page-nav-inbox-host {
    display: none;
}

/* The way back is the reader's too: the page tabs, `1`-`9` and Escape all
   reach the dashboard without it. */
body[data-show-dashboard-button="false"] .dashboard-link {
    display: none;
}

/* Hide cheatsheet button when showCheatSheetButton is false */
body[data-show-cheatsheet-button="false"] .help-button {
    display: none;
}

/* Hide add bookmark button when showAddBookmarkButton is false */
body[data-show-add-bookmark-button="false"] #quick-add-toolbar-btn {
    display: none;
}

/* Hide search button when showSearchButton is false */
body[data-show-search-button="false"] #search-button {
    display: none;
}

/* Hide finders button when showFindersButton is false */
body[data-show-finders-button="false"] .finders-button {
    display: none;
}

/* Hide commands button when showCommandsButton is false */
body[data-show-commands-button="false"] .commands-button {
    display: none;
}

/* Hide recent button when showRecentButton is false */
body[data-show-recent-button="false"] .recent-button {
    display: none;
}

/* Hide fold-all button when showCollapseAllButton is false */
body[data-show-collapse-all-button="false"] #collapse-all-button {
    display: none;
}

/* Collapse the optional secondary toolbar only when all three of its buttons
   are off. Fold-all used to have no setting of its own and rode along with this
   rule, so turning off Recent and Help took it away too — and turning Help back
   on brought it back. Each button now answers only to its own toggle. */
body[data-show-recent-button="false"][data-show-cheatsheet-button="false"][data-show-collapse-all-button="false"] .btn-group-secondary {
    display: none !important;
}

/* The quick-action toolbar (add / search / commands / finders / recent / help /
   fold, plus the search-flow-hint that advertises their keys) belongs to the
   bookmarks dashboard. Gone in every full-container view, keyed off the same
   layout class each one already stamps on #dashboard-layout -- no new class or
   attribute needed. :has() reading #dashboard-layout from up at body is the
   same trick fresh-tutorial.css and inbox-tutorial.css already use. display:none,
   not opacity, so Tab cannot land in it either. What's New (outside
   .button-container) is untouched. */
body:has(#dashboard-layout.config-layout) .button-container,
body:has(#dashboard-layout.inbox-layout) .button-container,
/* The actions moved into the header; the scoping had to move with them, or
   Health and Inbox draw a fold-all and an add-bookmark for a grid that is not
   there. The destinations beside them stay: those are how you leave. */
/*
 * The actions stand in every view.
 *
 * They were hidden in config, health and inbox, from back when they were a
 * dock that belonged to the grid. In the header they are chrome like the
 * destinations beside them: add, search, recents and the cheat sheet all open
 * over whatever view you are in{
    display: flex !important;
}

/* The three shortcut-label modes live together in dashboard-bookmark-row.css,
   which loads after this file and owns the row's hover and selection chrome --
   and "hover" is a hover state. Nothing here. */

/* Hide pin icon when showPinIcon is false */
body[data-show-pin-icon="false"] .bookmark-pin-badge {
    display: none;
}

/* Hide note icon when showNoteIcon is false */
body[data-show-note-icon="false"] .bookmark-note-badge {
    display: none;
}

/* Terminal layout variants */
body[data-show-shortcuts="false"] .dashboard-grid.layout-terminal .bookmark-link,
body[data-show-pin-icon="false"] .dashboard-grid.layout-terminal .bookmark-link,
body[data-show-note-icon="false"] .dashboard-grid.layout-terminal .bookmark-link {
    --bookmark-row-gap: var(--space-1, 0.28rem);
    gap: var(--bookmark-row-gap);
}

body[data-show-shortcuts="false"][data-show-pin-icon="false"] .dashboard-grid.layout-terminal .bookmark-link {
    --bookmark-row-gap: var(--space-1, 0.24rem);
    gap: var(--bookmark-row-gap);
}

/* Recent bookmarks modal */
/*
 * Recent bookmarks, drawn the way the overlays spec draws a panel.
 *
 * It used to be a 52rem sheet of cards: every row an outlined box with the
 * name on one line and its category under it, a pill for the time and a count
 * beside that, and the row opened with its own position in the list. Four
 * bookmarks filled a window. The spec draws this as a narrow panel of quiet
 * rows in one slab -- icon, name, category, time on a single line -- which is
 * a list you read down rather than four cards you read one at a time.
 */
.modal.recent-bookmarks-modal {
    max-height: 88vh;
    overflow: hidden;
    max-width: min(27.5rem, calc(100vw - 2.5rem));
    width: min(27.5rem, calc(100vw - 2.5rem));
}

/* The header carries the name, the key, and the way out. */
.modal.recent-bookmarks-modal .modal-header {
    display: flex;
    align-items: center;
    gap: var(--space-3, 0.75rem);
    padding-bottom: var(--space-3, 0.75rem);
    border-bottom: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
}

.modal.recent-bookmarks-modal .modal-title {
    /* The app lower-cases everywhere it names something. */
    text-transform: lowercase;
    font-size: var(--font-size-text);
    font-weight: var(--font-weight-semibold, 600);
}

.recent-bookmarks-modal-key {
    flex-shrink: 0;
    padding: 0.15rem 0.4rem;
    border-radius: var(--radius-3, 4px);
    background: color-mix(in srgb, var(--accent-primary) 14%, transparent);
    color: var(--accent-primary);
    font-size: var(--font-size-small, 0.72rem);
    font-weight: 700;
}

.recent-bookmarks-modal-close {
    margin-left: auto;
    flex-shrink: 0;
    border: none;
    background: transparent;
    color: var(--text-tertiary);
    font-family: inherit;
    font-size: var(--font-size-small, 0.72rem);
    opacity: 0.5;
    cursor: pointer;
}

.recent-bookmarks-modal-close:hover,
.recent-bookmarks-modal-close:focus-visible {
    opacity: 1;
}

/*
 * The panel closes with the key in its own header, so the wide Close button
 * and the hint under it are saying a third time what the header already says.
 */
.modal.recent-bookmarks-modal .modal-actions,
#app-modal:has(.recent-bookmarks-modal) .modal-esc-hint {
    display: none;
}

.modal.recent-bookmarks-modal .modal-body {
    margin-top: var(--space-4, 1rem);
    margin-bottom: 0;
}

/*
 * Opening the whole list is an offer, not the subject.
 *
 * The button ran the full width of the panel above the list, so the loudest
 * thing on a panel about four bookmarks was a button about all of them. Small
 * and left, under the list it acts on.
 */
.recent-bookmarks-modal-toolbar {
    display: flex;
    flex-direction: column;
    gap: var(--space-1, 0.3rem);
    margin: var(--space-3, 0.75rem) 0 0;
}

.recent-bookmarks-open-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2, 0.5rem);
}

.recent-bookmarks-open-btn.modal-button {
    flex: 0 0 auto;
    min-width: 0;
    padding: 0.2rem 0.55rem;
    font-size: var(--font-size-small, 0.72rem);
    justify-content: center;
}

.recent-bookmarks-open-hint {
    margin: 0;
    color: var(--text-tertiary);
    font-size: var(--font-size-small, 0.72rem);
    line-height: 1.35;
    opacity: 0.7;
}

/*
 * One slab holding the rows, rather than a row holding its own box.
 *
 * A list of ten outlined cards is ten borders competing with the panel's own;
 * a single lifted surface under them lets the rows be rows.
 *
 * The surface itself comes from the shared overlay-slab rule in modal.css,
 * which the cheat sheet and the tag cloud already stand on and which knows not
 * to paint on a flat theme. Only the layout is here.
 */
.recent-bookmarks-modal-list {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    max-height: 52vh;
    overflow-y: auto;
    padding: var(--space-2, 0.5rem);
}

.recent-bookmarks-modal-item {
    display: grid;
    grid-template-columns: 1.1rem minmax(0, 1fr) auto auto auto;
    align-items: center;
    gap: 0.6rem;
    border-radius: var(--radius-3, 4px);
    padding: 0.3rem 0.5rem;
    color: var(--text-primary);
    font-size: var(--font-size-controls);
    text-decoration: none;
    transition: background 0.1s ease, color 0.1s ease;
}

/*
 * Hover and the keyboard land on the same row treatment.
 *
 * Selection in this panel *is* focus -- the arrow keys move focus down the
 * list -- so the selected row of the spec is the focused row here, down to the
 * accent bar on its left edge and the glow the theme's depth decides.
 */
.recent-bookmarks-modal-item:hover,
.recent-bookmarks-modal-item:focus-visible {
    transform: none;
    outline: none;
    background: color-mix(in srgb, var(--accent-primary) 12%, transparent);
}

.recent-bookmarks-modal-item:focus-visible {
    box-shadow:
        inset 3px 0 0 var(--accent-primary),
        0 0 12px color-mix(in srgb, var(--accent-primary) calc(28% * var(--theme-depth, 1)), transparent);
}

.recent-bookmarks-modal-icon {
    width: 1.1rem;
    height: 1.1rem;
    flex-shrink: 0;
    object-fit: contain;
    border-radius: 2px;
}

.recent-bookmarks-modal-icon--letter {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: color-mix(in srgb, var(--text-primary) 10%, transparent);
    color: var(--text-tertiary);
    font-size: var(--font-size-small, 0.65rem);
    font-weight: 700;
}

.recent-bookmarks-modal-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.recent-bookmarks-modal-detail {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text-tertiary);
    font-size: var(--font-size-small, 0.65rem);
    opacity: 0.55;
}

/*
 * The time is text, not a badge.
 *
 * It wore a bordered pill in the accent colour, which made "4m ago" the
 * loudest thing in a row whose subject is the bookmark.
 */
.recent-bookmarks-modal-recency {
    color: var(--text-secondary);
    font-size: var(--font-size-small, 0.65rem);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    opacity: 0.75;
}

.recent-bookmarks-modal-opens {
    color: var(--text-tertiary);
    font-size: var(--font-size-small, 0.65rem);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    opacity: 0.55;
}

/* The panel's own foot: what is on screen, and what Enter does with it. */
.recent-bookmarks-modal-foot {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-3, 0.75rem);
    margin-top: var(--space-3, 0.75rem);
    padding-top: var(--space-3, 0.75rem);
    border-top: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
    color: var(--text-tertiary);
    font-size: var(--font-size-small, 0.65rem);
    opacity: 0.6;
}

.recent-bookmarks-modal-foot .recent-bookmarks-modal-key {
    background: color-mix(in srgb, var(--text-primary) 12%, transparent);
    color: inherit;
    padding: 0.1rem 0.35rem;
}

.recent-bookmarks-empty {
    padding: var(--space-5, 1.5rem) var(--space-2, 0.5rem);
    color: var(--text-tertiary);
    font-size: var(--font-size-text);
    text-align: center;
}

/* Dashboard toolbar modals — same placement and chrome as search overlay */
body:has(#dashboard-layout) #app-modal.modal-overlay:has(.recent-bookmarks-modal),
body:has(#dashboard-layout) #app-modal.modal-overlay:has(.keyboard-cheat-sheet-modal),
body:has(#dashboard-layout) #app-modal.modal-overlay:has(.whats-new-modal) {
    justify-content: flex-end;
    align-items: center;
    padding: 0 max(1.25rem, 2vw) calc(var(--dashboard-toolbar-stack, 5.75rem) + env(safe-area-inset-bottom, 0px));
    box-sizing: border-box;
    backdrop-filter: blur(var(--modal-scrim-blur));
    -webkit-backdrop-filter: blur(var(--modal-scrim-blur));
}

/* Not a sheet: one that hangs from the header sizes itself against the header
   row, and a selector this specific would out-shout it. */
body:has(#dashboard-layout) #app-modal.modal-overlay:has(.recent-bookmarks-modal) .modal:not(.header-sheet),
body:has(#dashboard-layout) #app-modal.modal-overlay:has(.keyboard-cheat-sheet-modal) .modal,
body:has(#dashboard-layout) #app-modal.modal-overlay:has(.whats-new-modal) .modal {
    width: min(52rem, calc(100vw - 2.5rem));
    max-width: 100%;
    margin: 0;
    max-height: min(70vh, calc(100vh - var(--dashboard-toolbar-stack, 5.75rem) - 2.5rem));
    transform: translateY(10px) scale(0.98);
    transition: transform 0.15s ease-in, opacity 0.15s ease-in;
}

/* What's new is narrower than the two it shares the shell with: it is a single
   column of prose, and 52rem ran its lines to ~84 characters. Split out rather
   than folded into the rule above, because the recent-bookmarks list and the
   cheat sheet's two-column key table both still want the width. This selector
   has to repeat the full :has() chain — the shorter .modal.whats-new-modal in
   modal.css loses to it on specificity. */
body:has(#dashboard-layout) #app-modal.modal-overlay:has(.whats-new-modal) .modal {
    width: min(36rem, calc(100vw - 2.5rem));
}

/* The release notes themselves, as against the notices that borrow the shell:
   wide and tall at a fixed size, so new changes sit side by side as cards and
   the modal does not jump when the section menu narrows the list. Lines stay
   short because the cards and the two columns of fixes divide the width. */
body:has(#dashboard-layout) #app-modal.modal-overlay:has(.whats-new-modal:not(.analytics-notice-modal)) .modal {
    width: min(64rem, calc(100vw - 2.5rem));
    height: min(52rem, calc(100vh - var(--dashboard-toolbar-stack, 5.75rem) - 4rem));
    max-height: calc(100vh - var(--dashboard-toolbar-stack, 5.75rem) - 4rem);
}

/*
 * Room above the modal as well as below it.
 *
 * The block above leaves 2rem under the modal and nothing over it, and caps the
 * height at 100vh - 2rem — so a modal tall enough to reach its own limit runs
 * into the top edge of the window while sitting a comfortable distance off the
 * bottom. Measured on a 900px window: 868 tall, 32px below, 0 above.
 *
 * The padding is symmetric and the height leaves room for both. Scoped to
 * what's new; recent bookmarks and the cheat sheet share the block above and
 * the same lopsidedness, but neither was what anybody was looking at.
 */
body:has(#dashboard-layout) #app-modal.modal-overlay:has(.whats-new-modal) {
    padding-top: max(var(--space-6, 2rem), env(safe-area-inset-top, 0px));
}

body:has(#dashboard-layout) #app-modal.modal-overlay.show:has(.recent-bookmarks-modal) .modal:not(.header-sheet),
body:has(#dashboard-layout) #app-modal.modal-overlay.show:has(.keyboard-cheat-sheet-modal) .modal,
body:has(#dashboard-layout) #app-modal.modal-overlay.show:has(.whats-new-modal) .modal {
    transform: translateY(0) scale(1);
    transition: transform 0.38s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.22s ease-out;
}

/* The header sheets keep their transform: it is their centring, not motion. */
body.no-animations:has(#dashboard-layout) #app-modal.modal-overlay:has(.recent-bookmarks-modal) .modal:not(.header-sheet),
body.no-animations:has(#dashboard-layout) #app-modal.modal-overlay:has(.keyboard-cheat-sheet-modal) .modal,
body.no-animations:has(#dashboard-layout) #app-modal.modal-overlay:has(.whats-new-modal) .modal {
    transform: none !important;
    transition: opacity 0.15s ease !important;
}

/* Inbox intro modal — centered above the inbox feed */

/* Page overview modal — centered compact shell */
/*
 * The pages panel wears the same chrome as recent bookmarks.
 *
 * Both are a short list you open with one key and leave with another, so they
 * are drawn the same: the name with its key beside it, a way out on the right,
 * the rows in one slab, and a thin foot. It named itself and then said "close"
 * three times -- a full-width button, an ESC hint under it, and nothing in the
 * header.
 */
.modal.page-overview-modal .modal-header {
    display: flex;
    align-items: center;
    gap: var(--space-3, 0.75rem);
    padding-bottom: var(--space-3, 0.75rem);
    border-bottom: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
}

.modal.page-overview-modal .modal-title {
    text-transform: lowercase;
    font-size: var(--font-size-text);
    font-weight: var(--font-weight-semibold, 600);
}

.page-overview-modal-key {
    flex-shrink: 0;
    padding: 0.15rem 0.4rem;
    border-radius: var(--radius-3, 4px);
    background: color-mix(in srgb, var(--accent-primary) 14%, transparent);
    color: var(--accent-primary);
    font-size: var(--font-size-small, 0.72rem);
    font-weight: 700;
}

.page-overview-modal-close {
    margin-left: auto;
    flex-shrink: 0;
    border: none;
    background: transparent;
    color: var(--text-tertiary);
    font-family: inherit;
    font-size: var(--font-size-small, 0.72rem);
    opacity: 0.5;
    cursor: pointer;
}

.page-overview-modal-close:hover,
.page-overview-modal-close:focus-visible {
    opacity: 1;
}

.modal.page-overview-modal .modal-actions,
#app-modal:has(.page-overview-modal) .modal-esc-hint {
    display: none;
}

.page-overview-modal-foot {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-3, 0.75rem);
    margin-top: var(--space-3, 0.75rem);
    padding-top: var(--space-3, 0.75rem);
    border-top: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
    color: var(--text-tertiary);
    font-size: var(--font-size-small, 0.65rem);
    opacity: 0.6;
}

.page-overview-modal-footkey {
    padding: 0.1rem 0.35rem;
    border-radius: 2px;
    background: color-mix(in srgb, var(--text-primary) 12%, transparent);
}

body:has(#dashboard-layout) #app-modal.modal-overlay:has(.page-overview-modal) {
    justify-content: center;
    align-items: center;
    padding: max(1.25rem, 2vw);
    box-sizing: border-box;
    backdrop-filter: blur(var(--modal-scrim-blur));
    -webkit-backdrop-filter: blur(var(--modal-scrim-blur));
}

/* Not the sheet: it sizes itself against the header row it hangs from, and a
   selector this specific would out-shout it. */
body:has(#dashboard-layout) #app-modal.modal-overlay:has(.page-overview-modal) .modal:not(.page-overview-sheet) {
    width: min(22rem, calc(100vw - 2.5rem));
    max-width: 100%;
    margin: 0;
    max-height: min(70vh, calc(100vh - 4rem));
    transform: translateY(10px) scale(0.98);
    transition: transform 0.15s ease-in, opacity 0.15s ease-in;
}

body:has(#dashboard-layout) #app-modal.modal-overlay.show:has(.page-overview-modal) .modal:not(.page-overview-sheet) {
    transform: translateY(0) scale(1);
    transition: transform 0.38s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.22s ease-out;
}

body.no-animations:has(#dashboard-layout) #app-modal.modal-overlay:has(.page-overview-modal) .modal:not(.page-overview-sheet) {
    transform: none !important;
    transition: opacity 0.15s ease !important;
}

.recent-bookmarks-skeleton {
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 0.5rem);
}

.recent-bookmarks-skeleton-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-3, 0.75rem);
    border: 1px solid var(--border-primary);
    background: var(--background-primary);
    padding: var(--space-2, 0.55rem) var(--space-3, 0.7rem);
    border-radius: var(--layout-radius-sm, 8px);
}

.recent-bookmarks-skeleton-rank {
    width: 1.35rem;
    height: 0.75em;
    border-radius: var(--radius-2, 3px);
    background: var(--background-secondary);
}

.recent-bookmarks-skeleton-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-1, 0.25rem);
    min-width: 0;
}

.recent-bookmarks-skeleton-stats {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: var(--space-1, 0.2rem);
}

.recent-bookmarks-skeleton-name,
.recent-bookmarks-skeleton-detail,
.recent-bookmarks-skeleton-recency,
.recent-bookmarks-skeleton-opens {
    display: block;
    border-radius: var(--radius-2, 3px);
    background: linear-gradient(
        90deg,
        var(--background-secondary) 25%,
        color-mix(in srgb, var(--background-secondary) 60%, var(--text-tertiary) 40%) 50%,
        var(--background-secondary) 75%
    );
    background-size: 200% 100%;
    animation: skeletonShimmer 1.4s ease-in-out infinite;
}

.recent-bookmarks-skeleton-name {
    height: 0.85em;
    width: 72%;
}

.recent-bookmarks-skeleton-detail {
    height: 0.72em;
    width: 42%;
}

.recent-bookmarks-skeleton-recency {
    height: 1.15rem;
    width: 3.25rem;
}

.recent-bookmarks-skeleton-opens {
    height: 0.72em;
    width: 1.5rem;
}

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

@keyframes nextdashStaleFlash {
    0% {
        box-shadow: 0 0 0 0 var(--accent-primary, rgba(100, 180, 255, 0.65));
    }
    40% {
        box-shadow: 0 0 0 6px rgba(100, 180, 255, 0.15);
    }
    100% {
        box-shadow: 0 0 0 0 transparent;
    }
}

.category.nextdash-stale-flash {
    animation: nextdashStaleFlash 1.1s ease-out 2;
    border-radius: var(--radius-4, 6px);
}

@keyframes bookmarkDeepLinkFocus {
    0% {
        box-shadow: inset 3px 0 0 var(--accent-primary), 0 0 0 0 color-mix(in srgb, var(--accent-primary) 35%, transparent);
    }
    40% {
        box-shadow: inset 3px 0 0 var(--accent-primary), 0 0 0 6px color-mix(in srgb, var(--accent-primary) 22%, transparent);
    }
    100% {
        box-shadow: inset 3px 0 0 transparent, 0 0 0 0 transparent;
    }
}

.bookmark-link.bookmark-deep-link-focus {
    animation: bookmarkDeepLinkFocus 1.1s ease-out 2;
    border-radius: var(--layout-radius-sm, 8px);
}

body.no-animations .bookmark-link.bookmark-deep-link-focus {
    animation: none;
    box-shadow: inset 3px 0 0 var(--accent-primary);
}

body.no-animations #dashboard-layout.page-transition,
body.no-animations .category.animate-enter,
body.no-animations .bookmark-link.animate-enter,
body.no-animations .bookmark-inline-form,
body.no-animations .bookmark-move-out {
    animation: none !important;
    transform: none !important;
    opacity: 1 !important;
}

/* ── Quick-add omnibox ──────────────────────────────────────────────────── */

.omnibox-overlay {
    position: fixed;
    inset: 0;
    z-index: 1200;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    padding: 0 max(1.25rem, 2vw) calc(var(--dashboard-toolbar-stack, 5.75rem) + env(safe-area-inset-bottom, 0px));
    box-sizing: border-box;
    pointer-events: none;
    opacity: 0;
    background: var(--modal-scrim);
    backdrop-filter: blur(var(--modal-scrim-blur));
    -webkit-backdrop-filter: blur(var(--modal-scrim-blur));
    transition: opacity 0.2s ease-out;
}

.omnibox-overlay.is-visible {
    opacity: 1;
    pointer-events: auto;
}

.omnibox-box {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.6rem);
    width: min(52rem, calc(100vw - 2.5rem));
    max-width: 100%;
    box-sizing: border-box;
    padding: var(--space-3, 0.75rem) 1.25rem;
    border: 1px solid color-mix(in srgb, var(--border-primary) 75%, transparent);
    border-radius: var(--layout-radius-md, 12px);
    background: color-mix(in srgb, var(--background-modal) 90%, transparent);
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    box-shadow: 0 24px 64px rgba(0, 0, 0, 0.28), 0 2px 8px rgba(0, 0, 0, 0.12);
    /* exit: slide down toward toolbar */
    transform: translateY(10px) scale(0.98);
    opacity: 0;
    transition: transform 0.15s ease-in, opacity 0.15s ease-in;
}

.omnibox-overlay.is-visible .omnibox-box {
    transform: translateY(0) scale(1);
    opacity: 1;
    /* entrance: spring — vertraagt soepel aan het einde */
    transition: transform 0.38s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.22s ease-out;
}

.omnibox-hint {
    font-family: "Source Code Pro", monospace;
    font-size: var(--text-xs, 0.72rem);
    color: var(--text-tertiary);
    white-space: nowrap;
    flex-shrink: 0;
}

.omnibox-hint::before {
    content: '+';
    color: var(--accent-primary);
    margin-right: var(--space-1-5, 0.4rem);
    font-weight: var(--font-weight-bold);
}

.omnibox-input {
    flex: 1;
    background: transparent;
    border: none;
    outline: none;
    color: var(--text-primary);
    font-family: "Source Code Pro", monospace;
    font-size: var(--text-base, 0.9rem);
    padding: 0;
    caret-color: var(--accent-primary);
}

.omnibox-input::placeholder {
    color: var(--text-tertiary);
    opacity: 0.5;
}

.omnibox-status {
    font-size: var(--text-xs, 0.72rem);
    color: var(--text-tertiary);
    white-space: nowrap;
    flex-shrink: 0;
    min-width: 5rem;
    text-align: right;
}

.omnibox-status.is-error {
    color: var(--accent-error);
}

@media (prefers-reduced-motion: reduce) {
    /* Spring drag-and-drop placeholder → instant appear, no scale */
    .bookmark-drop-placeholder,
    .bookmarks-list.reorder-container .bookmark-drop-placeholder {
        animation: none !important;
        opacity: 0.65 !important;
        transform: none !important;
    }
    /* Drag reorder row transitions */
    .bookmarks-list.reorder-container .bookmark-link,
    .bookmarks-list.reorder-container .bookmark-link.bookmark-move-in,
    .bookmarks-list.reorder-container::after {
        transition: none !important;
        animation: none !important;
    }
    /* Skeleton shimmer → static bar */
    .recent-bookmarks-skeleton-name,
    .recent-bookmarks-skeleton-detail,
    .recent-bookmarks-skeleton-recency,
    .recent-bookmarks-skeleton-opens { animation: none !important; }
    /* Page / category / bookmark entrance animations */
    #dashboard-layout.page-transition,
    .category.animate-enter,
    .bookmark-link.animate-enter,
    .bookmark-inline-form { animation: none !important; transform: none !important; opacity: 1 !important; }

    /* Near-zero rather than `none`, matching reduced-motion.css: each of these
       classes is removed again by an `animationend` listener with { once: true }
       (bookmark-move-out in dashboard-inline-edit, bookmark-copy-flash in four
       copy paths, bookmark-pulse in dashboard-bookmark-rows, deep-link-focus in
       dashboard-page-nav). `animation: none` fires no event, so the listener
       never ran and the class stayed on the row for the rest of the session.
       There is nothing to see either way — the animation is over in a frame. */
    .bookmark-move-out { animation-duration: 0.01ms !important; transform: none !important; opacity: 1 !important; }
    .bookmark-link.bookmark-deep-link-focus,
    .bookmark-link.bookmark-copy-flash::after,
    .layout-launcher .bookmark-link.bookmark-pulse {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
    }
    /* Date popover entrance */
    .date-popover { animation: none !important; }
}

/* ─── Row tag chips ──────────────────────────────────────────────────────── */

/* Inside the link, not a grid column: .bookmark-link is a subgrid whose columns
   line up across every category on the page, so a new column would move every
   row. Same chip as the inbox (.inbox-item-tag) and Config (.config-bm-tag)
   — a tag should look like a tag wherever it is listed. */
.bookmark-tag-strip {
    display: inline-flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: var(--space-1, 0.25rem);
    margin-left: var(--space-1-5, 0.4rem);
    min-width: 0;
    /* Never at the cost of the name: the strip gives way first. */
    flex: 0 1 auto;
    overflow: hidden;
}

.bookmark-tag-chip {
    padding: var(--space-hairline, 0.05rem) var(--space-2, 0.45rem);
    border-radius: var(--radius-pill, 999px);
    border: 1px solid var(--border-primary);
    background: var(--background-secondary);
    color: var(--text-secondary);
    font-size: var(--text-2xs, 0.68rem);
    font-weight: var(--font-weight-normal, 400);
    line-height: 1.5;
    white-space: nowrap;
    max-width: 8rem;
    overflow: hidden;
    text-overflow: ellipsis;
}

.bookmark-tag-chip--more {
    font-variant-numeric: tabular-nums;
    opacity: 0.75;
}

/* Terminal and list presets are deliberately dense; a chip there costs more
   than it says, so it drops to text weight instead of a pill. */
.dashboard-grid.layout-terminal .bookmark-tag-chip,
.dashboard-grid.layout-list .bookmark-tag-chip {
    border: 0;
    background: none;
    padding: 0 var(--space-hairline, 0.15rem);
    opacity: 0.7;
}

@media (max-width: 720px) {
    /* On a phone the name is the only thing with room. */
    .bookmark-tag-strip { display: none; }
}

/* g is a chord: for three seconds after it, the next key means "top" (g),
   "first pinned" (p) or a category number. Nothing said so, so the only way to
   learn it was to press something and watch. The pill is the key itself, which
   needs no translation. */
body[data-g-chord="armed"]::after {
    content: "g…";
    position: fixed;
    right: 1rem;
    bottom: 1rem;
    z-index: 60;
    padding: var(--space-1, 0.2rem) var(--space-2, 0.55rem);
    border: 1px solid color-mix(in srgb, var(--accent-primary) 40%, transparent);
    border-radius: var(--radius-pill, 999px);
    background: color-mix(in srgb, var(--accent-primary) 14%, var(--background-secondary));
    color: var(--accent-primary);
    font-family: var(--font-family-main);
    font-size: var(--text-xs, 0.75rem);
    letter-spacing: 0.04em;
    pointer-events: none;
}

/* The key legend under the grid, matching the one health and inbox carry under
   their feeds. Decorative: every key it lists is in the cheat sheet, and the
   rows announce what they do on their own. */
.dashboard-legend {
    display: none;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-1, 0.3rem) var(--space-4, 0.9rem);
    margin: var(--space-4, 1rem) 0 var(--space-1, 0.25rem);
    font-size: var(--text-xs, 0.72rem);
    color: var(--text-tertiary);
    opacity: 0.75;
}

/* Only once the keyboard is in use: KeyboardNavigation stamps this at the first
   cursor move and clears it on Enter. Without it the line sat under the grid
   all day — under seven bookmarks, as tall as the thing it explained. No rule
   above it either, which is what made it read as a section of its own. */
body[data-grid-keys="shown"] .dashboard-legend {
    display: flex;
}

.dashboard-legend kbd {
    font-size: var(--text-xs, 0.7rem);
    padding: var(--space-hairline, 0.05rem) var(--space-1-5, 0.32rem);
    border-radius: var(--radius-3, 0.25rem);
    border: 1px solid var(--border-secondary);
    background: var(--background-secondary);
    color: var(--text-secondary);
}

/* The category icon editor: one field beside the header it changes, the same
   shape the page-tab popover uses. Category.Icon has been stored and rendered
   for a long time with nowhere to set it — eight categories on a page meant
   eight identical ⊣ headers. */
.category-icon-popover {
    position: absolute;
    z-index: 70;
    display: flex;
    align-items: center;
    gap: var(--space-1-5, 0.35rem);
    padding: var(--space-1-5, 0.35rem) var(--space-2, 0.45rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-4, 0.4rem);
    background: var(--background-secondary);
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
}

.category-icon-input {
    width: 3.2rem;
    padding: var(--space-hairline, 0.15rem) var(--space-1-5, 0.35rem);
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-3, 0.3rem);
    background: var(--background-primary);
    color: var(--text-primary);
    font: inherit;
    text-align: center;
}

.category-icon-clear {
    padding: var(--space-hairline, 0.15rem) var(--space-2, 0.45rem);
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-3, 0.3rem);
    background: transparent;
    color: var(--text-secondary);
    font: inherit;
    font-size: var(--text-sm, 0.78rem);
    cursor: pointer;
}

.category-icon-clear:hover,
.category-icon-clear:focus-visible {
    color: var(--accent-primary);
    border-color: color-mix(in srgb, var(--accent-primary) 45%, transparent);
}

/* ── Filter this page (Shift+F) ─────────────────────────────────────────────
   A slim bar above the grid, not a panel: it narrows what is already on screen
   and disappears again with one Escape. Rows and empty categories are hidden
   with a class rather than re-rendered, so the layout, the keyboard cursor and
   any selection survive the filter. */
.grid-filter-bar {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    /* It sits above the grid rather than in it. The width and the offset are
       measured from the grid and handed here by dashboard-grid-filter.js: the
       grid's own width comes from how many fixed-width columns fit, which no
       rule here can work out. Falls back to the container while they are unset,
       which is what it always did. */
    width: var(--grid-filter-width, 100%);
    box-sizing: border-box;
    /* The offset comes after the shorthand, which would otherwise reset it. */
    margin: 0 0 var(--space-2, 0.6rem);
    margin-left: var(--grid-filter-offset, 0);
    padding: var(--space-1, 0.3rem) var(--space-2, 0.5rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--layout-radius-sm, 8px);
    background: var(--background-secondary);
}

.grid-filter-input {
    flex: 1 1 auto;
    min-width: 0;
    border: 0;
    background: transparent;
    color: var(--text-primary);
    font-family: inherit;
    font-size: var(--text-sm, 0.85rem);
    padding: var(--space-hairline, 0.15rem) 0;
}

.grid-filter-input:focus {
    outline: none;
}

.grid-filter-count {
    color: var(--text-tertiary);
    font-size: var(--text-xs, 0.75rem);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.grid-filter-clear {
    border: 0;
    background: transparent;
    color: var(--text-secondary);
    font-size: var(--text-md, 1rem);
    line-height: 1;
    cursor: pointer;
    padding: 0 var(--space-1, 0.2rem);
}

.grid-filter-clear:hover {
    color: var(--text-primary);
}

.bookmark-link.grid-filter-hidden,
.category.grid-filter-empty {
    display: none !important;
}

/* "Publishes, but nothing new" — the quiet half of Fresh, off unless asked for.
   A dot rather than a number, and dimmer than a count: it says this row takes
   part, which is worth a glance and never worth reading. */
.bookmark-fresh-badge.is-quiet {
    color: color-mix(in srgb, var(--accent-primary) 55%, var(--text-tertiary));
    border-color: var(--border-secondary);
    background: transparent;
    letter-spacing: 0;
}

/*
 * A drawn menu icon sits on the text baseline of a row whose other icons are
 * glyphs, so it is centred as a box rather than laid out as a letter.
 */
.move-popover-check--drawn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.move-popover-check--drawn svg {
    display: block;
}

/* ─── Paste choice modal ─────────────────────────────────────────────────── */
/* Lived in dashboard-inbox.css, which rides in the views bundle and is only
   fetched when Inbox, Health or Config is opened. This modal appears on the
   dashboard itself, on paste, so none of its rules had loaded yet and it drew
   as unstyled markup: the URL overlapping the question, both options collapsed
   into one line. It belongs with the view that shows it. */
.paste-choice-overlay {
    z-index: 12050;
}

.paste-choice-modal {
    max-width: 34rem;
}

.paste-choice-url {
    font-size: var(--text-sm, 0.82rem);
    color: var(--text-secondary);
    word-break: break-all;
    margin-bottom: var(--space-3, 0.65rem);
    padding: var(--space-2, 0.45rem) var(--space-2, 0.55rem);
    border-radius: var(--radius-4, 0.35rem);
    background: var(--background-secondary);
    border: 1px solid var(--border-secondary);
}

.paste-choice-lead {
    margin: 0 0 var(--space-3, 0.75rem);
    color: var(--text-primary);
}

.paste-choice-options {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-3, 0.65rem);
}

.paste-choice-card {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-1, 0.2rem);
    padding: var(--space-3, 0.75rem) var(--space-3, 0.8rem) var(--space-3, 0.8rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--layout-radius-sm, 8px);
    background: var(--background-secondary);
    cursor: pointer;
    text-align: left;
    transition: border-color 0.15s ease, transform 0.15s ease;
}

.paste-choice-card:hover {
    border-color: color-mix(in srgb, var(--accent-primary) 50%, transparent);
    transform: translateY(-1px);
}

.paste-choice-card-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--accent-primary);
    font-size: var(--text-lg, 1.1rem);
}

.paste-choice-card-icon svg {
    display: block;
    width: 20px;
    height: 20px;
}

.paste-choice-card-title {
    font-weight: var(--font-weight-semibold);
    color: var(--text-primary);
}

.paste-choice-card-hint {
    font-size: var(--text-sm, 0.78rem);
    color: var(--text-secondary);
    line-height: 1.35;
}

.paste-choice-kbd {
    position: absolute;
    top: 0.45rem;
    right: 0.45rem;
    font-size: var(--text-2xs, 0.68rem);
    padding: var(--space-hairline, 0.1rem) var(--space-1, 0.3rem);
    border-radius: var(--radius-3, 0.25rem);
    border: 1px solid var(--border-secondary);
    color: var(--text-tertiary);
}

.paste-choice-remember {
    display: flex;
    align-items: center;
    gap: var(--space-1-5, 0.4rem);
    margin-top: var(--space-3, 0.85rem);
    font-size: var(--text-sm, 0.82rem);
    color: var(--text-secondary);
    cursor: pointer;
}

/* Layout A from docs/paste-preview-mockup.html: text left, picture right. */
.paste-choice-preview {
    display: flex;
    gap: var(--space-3, 0.75rem);
    margin: calc(-1 * var(--space-1, 0.25rem)) 0 var(--space-3, 0.65rem);
    padding: var(--space-2, 0.6rem);
    border-radius: var(--layout-radius-sm, 8px);
    background: var(--background-secondary);
    border: 1px solid var(--border-secondary);
}

.paste-choice-preview[hidden],
.paste-choice-preview [hidden] {
    display: none;
}

.paste-choice-preview-text {
    flex: 1;
    min-width: 0;
}

.paste-choice-preview-head {
    display: flex;
    align-items: center;
    gap: var(--space-1-5, 0.4rem);
    font-size: var(--text-2xs, 0.7rem);
    color: var(--text-tertiary);
}

.paste-choice-preview-fav {
    width: 14px;
    height: 14px;
    border-radius: 3px;
    flex: none;
}

.paste-choice-preview-title {
    margin: var(--space-1, 0.25rem) 0 var(--space-hairline, 0.1rem);
    font-weight: var(--font-weight-semibold);
    color: var(--text-primary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.paste-choice-preview-desc {
    font-size: var(--text-sm, 0.78rem);
    color: var(--text-secondary);
    line-height: 1.35;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.paste-choice-preview-thumb {
    width: 88px;
    height: 62px;
    flex: none;
    border-radius: var(--radius-4, 0.35rem);
    overflow: hidden;
    background: var(--background-primary);
}

.paste-choice-preview-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.paste-choice-preview.is-loading .paste-choice-preview-domain,
.paste-choice-preview.is-loading .paste-choice-preview-title,
.paste-choice-preview.is-loading .paste-choice-preview-desc,
.paste-choice-preview.is-loading .paste-choice-preview-thumb {
    border-radius: var(--radius-3, 0.25rem);
    background: linear-gradient(90deg,
        var(--background-primary),
        var(--border-secondary),
        var(--background-primary));
    background-size: 200% 100%;
    animation: paste-choice-shimmer 1.2s linear infinite;
}

.paste-choice-preview.is-loading .paste-choice-preview-domain { display: block; width: 40%; height: 0.6rem; }
.paste-choice-preview.is-loading .paste-choice-preview-title { width: 85%; height: 0.8rem; }
.paste-choice-preview.is-loading .paste-choice-preview-desc { width: 100%; height: 1.6rem; }

@keyframes paste-choice-shimmer {
    to { background-position: -200% 0; }
}

@media (max-width: 520px) {
    .paste-choice-preview-thumb {
        display: none;
    }

    .paste-choice-options {
        grid-template-columns: 1fr;
    }
}

/*
 * A widget's body: a card inside an otherwise frameless grid.
 *
 * Categories are deliberately unframed -- they are lists of links, and a box
 * around each would turn the page into a grid of boxes. A widget is the
 * exception that earns one: it is a panel of figures, not a list, and without
 * an edge it reads as loose text that happens to sit under a heading.
 *
 * The heading stays outside the card, so a widget's title lines up with every
 * category title beside it.
 */
/*
 * The surfaces a readout is built from: the page's own colour, one step off
 * it, and solid.
 *
 * They were tints of --text-primary, one step towards the reader. On every
 * theme that read as a grey-white panel pasted on the dashboard, and the cells
 * inside it lighter still: mixing in the ink mixes in its whiteness. These take
 * the page's hue and chroma and move only its lightness -- up on a dark theme,
 * down on a light one, as --ink-dir says -- so a tile keeps the colour of the
 * page it stands on. The theme's accent tint rides on top the way it does on
 * the surface ladder.
 *
 * The cells sit one step further out than the tile. Their container paints the
 * tile's own ground, so the 1px gaps between them draw the hairlines. The
 * solid cell is kept under its own name so glass can thin it without a
 * variable that refers to itself.
 *
 * Chroma is the page's own, unscaled: a neutral page has no hue to speak of,
 * and inventing one would tint a greyscale theme red.
 *
 * The fallback, for a browser without relative colours, is the old ink tint at
 * a lower strength over the solid page.
 */
.dashboard-widget {
    --dashboard-widget-surface: color-mix(in srgb, var(--text-primary) 4%, var(--background-primary));
    --dashboard-widget-cell-solid: color-mix(in srgb, var(--text-primary) 8%, var(--background-primary));
    --dashboard-widget-cell: var(--dashboard-widget-cell-solid);
    --dashboard-widget-rule: var(--dashboard-widget-surface);
    --dashboard-widget-cell-hover: color-mix(in srgb, var(--dashboard-widget-cell) 88%, var(--text-primary));
}

@supports (color: oklch(from white l c h)) {
    .dashboard-widget {
        --dashboard-widget-surface: color-mix(in srgb, var(--accent-primary) var(--theme-tint, 0%),
            oklch(from var(--background-primary) clamp(0, calc(l + 0.035 * var(--ink-dir, 1)), 1) c h));
        --dashboard-widget-cell-solid: color-mix(in srgb, var(--accent-primary) var(--theme-tint, 0%),
            oklch(from var(--background-primary) clamp(0, calc(l + 0.07 * var(--ink-dir, 1)), 1) c h));
    }
}

.dashboard-widget .dashboard-widget-body {
    padding: var(--space-3, 0.75rem) var(--space-3, 0.85rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-5, 9px);
    background: var(--dashboard-widget-surface);
    /*
     * Reserved before the data arrives.
     *
     * The packed layout measures block heights to place columns; a widget that
     * grew once its figures loaded would shift every column after it.
     */
    min-height: 3.5rem;
    /* The grid's own text size, not the smaller one used for meta lines: these
       are the widget's content, not a note about it. */
    font-size: var(--font-size-text);
    color: var(--text-primary);
}

/*
 * Nothing to report is a result, and it is the quietest one.
 *
 * These used to be an italic sentence at the tile's own text size, which on a
 * dashboard of thirteen tiles meant the ones with nothing to say were as loud
 * as the ones with something. Smaller, dimmer, and centred in the height the
 * tile reserves anyway -- so an empty tile reads as calm rather than as broken.
 *
 * Not italic: the grid is set in a monospaced face, and a slanted monospace at
 * small sizes is measurably harder to read than an upright one.
 */
.dashboard-widget .dashboard-widget-body--empty,
.dashboard-widget-waiting,
.dashboard-widget-empty,
.dashboard-widget-health-waiting {
    display: flex;
    align-items: center;
    margin: 0;
    min-height: 2rem;
    color: var(--text-tertiary);
    font-size: var(--font-size-small);
    font-style: normal;
    line-height: 1.45;
    text-wrap: pretty;
}

/* The Retry on a stopped feed: a quiet text button at the end of its row. */
.dashboard-widget-feeds-retry {
    flex-shrink: 0;
    margin-left: var(--space-2, 0.55rem);
    padding: 0;
    border: none;
    background: none;
    color: var(--text-secondary);
    font: inherit;
    font-size: var(--font-size-small);
    text-decoration: underline;
    cursor: pointer;
}

.dashboard-widget-feeds-retry:hover,
.dashboard-widget-feeds-retry:focus-visible {
    color: var(--text-primary);
}

.dashboard-widget-feeds-retry[disabled] {
    opacity: 0.5;
    cursor: default;
}

/* The notes widget: text, with tickable lines. */
.dashboard-widget-note-lines {
    display: block;
}

/* Block flow rather than a flex column, so two columns can share the text.
   :not(:last-child) also lifts it over the margin: 0 the rows set below. */
.dashboard-widget-note-lines > *:not(:last-child) {
    margin: 0 0 var(--space-1, 0.25rem);
}

.dashboard-widget-note-lines > * {
    break-inside: avoid;
}

.dashboard-widget-note-text {
    margin: 0;
    color: var(--text-primary);
    font-size: var(--font-size-small);
    line-height: 1.45;
    overflow-wrap: anywhere;
    white-space: pre-wrap;
}

.dashboard-widget-note-gap {
    min-height: 0.5rem;
}

.dashboard-widget-note-task {
    display: flex;
    align-items: baseline;
    gap: var(--space-2, 0.55rem);
    color: var(--text-primary);
    font-size: var(--font-size-small);
    line-height: 1.45;
    cursor: pointer;
}

.dashboard-widget-note-task span {
    min-width: 0;
    overflow-wrap: anywhere;
}

.dashboard-widget-note-done span {
    color: var(--text-tertiary);
    text-decoration: line-through;
}

.dashboard-widget-note-edit {
    align-self: flex-start;
    margin-top: var(--space-2, 0.55rem);
    padding: 0;
    border: none;
    background: none;
    color: var(--text-tertiary);
    font: inherit;
    font-size: var(--font-size-small);
    cursor: pointer;
}

.dashboard-widget-note-edit:hover,
.dashboard-widget-note-edit:focus-visible {
    color: var(--text-primary);
}

.dashboard-widget-note-area {
    width: 100%;
    box-sizing: border-box;
    resize: vertical;
    padding: var(--space-2, 0.55rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-4, 7px);
    background: var(--background-secondary);
    color: var(--text-primary);
    font: inherit;
    font-size: var(--font-size-small);
    line-height: 1.45;
}

.dashboard-widget-note-hint {
    margin: var(--space-1, 0.25rem) 0 0;
    color: var(--text-tertiary);
    font-size: var(--font-size-small);
}

/* Markdown in a note, and what a two-column tile adds. */
.dashboard-widget-note-heading { font-weight: 600; color: var(--text-primary); break-after: avoid; }
.dashboard-widget-note-h1 { font-size: var(--font-size-body, 1rem); }
.dashboard-widget-note-h2, .dashboard-widget-note-h3 { font-size: var(--font-size-small); }
.dashboard-widget-note-list { padding-left: 1.25rem; font-size: var(--font-size-small); line-height: 1.45; }
.dashboard-widget-note-quote { padding-left: var(--space-2, 0.55rem); border-left: 2px solid var(--border-primary); color: var(--text-secondary); font-size: var(--font-size-small); }
.dashboard-widget-note-code { overflow-x: auto; padding: var(--space-2, 0.55rem); border-radius: var(--radius-4, 7px); background: var(--background-secondary); font-size: var(--font-size-small); }
.dashboard-widget-note-text code, .dashboard-widget-note-task code, .dashboard-widget-note-list code { padding: 0 0.25rem; border-radius: 4px; background: var(--background-secondary); font-size: 0.95em; }
.dashboard-widget-note-table-wrap { overflow-x: auto; }
/* Links in a note take the theme's accent, not the browser's blue and purple. */
.dashboard-widget-note-lines a { color: var(--accent-primary); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--accent-primary) 40%, transparent); text-underline-offset: 0.15em; }
.dashboard-widget-note-lines a:visited { color: var(--accent-primary); }
.dashboard-widget-note-lines a:hover, .dashboard-widget-note-lines a:focus-visible { text-decoration-color: currentColor; }
.dashboard-widget-note-table { border-collapse: collapse; font-size: var(--font-size-small); }
.dashboard-widget-note-table th, .dashboard-widget-note-table td { padding: 0.15rem 0.5rem; border: 1px solid var(--border-primary); text-align: left; }
.dashboard-widget-note-actions { display: flex; align-items: baseline; gap: var(--space-3, 0.75rem); margin-top: var(--space-2, 0.55rem); }
.dashboard-widget-note-actions .dashboard-widget-note-edit { margin-top: 0; }
.dashboard-widget-note-expand { padding: 0; border: none; background: none; color: var(--text-tertiary); font: inherit; font-size: var(--font-size-small); cursor: pointer; }
.dashboard-widget-note-expand:hover, .dashboard-widget-note-expand:focus-visible { color: var(--text-primary); }
.dashboard-widget-note-stats { margin-left: auto; color: var(--text-tertiary); font-size: var(--font-size-small); white-space: nowrap; }
@container (min-width: 28rem) {
    .dashboard-widget-note-lines { column-count: 2; column-gap: 1.5rem; }
}

/* The slash-command menu in a notes editor. */
.notes-slash-menu { position: absolute; z-index: 5; min-width: 12rem; max-height: 14rem; overflow-y: auto; padding: 0.25rem; border: 1px solid var(--border-primary); border-radius: var(--radius-4, 7px); background: var(--background-primary); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18); font-size: var(--font-size-small); }
.notes-slash-menu[hidden] { display: none; }
.notes-slash-item { display: flex; justify-content: space-between; gap: 1rem; padding: 0.3rem 0.5rem; border-radius: 5px; cursor: pointer; color: var(--text-primary); }
.notes-slash-item[aria-selected="true"] { background: var(--background-secondary); }
.notes-slash-hint { color: var(--text-tertiary); }

/* The notes widget, large: toolbar, editor and live preview. */
.notes-modal-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 2100; display: flex; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.5); }
.notes-modal { display: flex; flex-direction: column; width: min(64rem, calc(100vw - 2rem)); height: min(40rem, calc(100vh - 2rem)); border: 1px solid var(--border-primary); border-radius: var(--radius-8, 12px); background: var(--background-primary); overflow: hidden; }
.notes-modal-toolbar { display: flex; align-items: center; gap: 0.25rem; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border-primary); flex-wrap: wrap; }
.notes-modal-tool, .notes-modal-close, .notes-modal-tab { min-width: 2rem; padding: 0.25rem 0.5rem; border: none; border-radius: 6px; background: none; color: var(--text-secondary); font: inherit; cursor: pointer; }
.notes-modal-tool:hover, .notes-modal-close:hover, .notes-modal-tab:hover, .notes-modal-tab.is-active { background: var(--background-secondary); color: var(--text-primary); }
.notes-modal-count { margin-left: auto; color: var(--text-tertiary); font-size: var(--font-size-small); }
.notes-modal-tabs { display: none; }
.notes-modal-body { display: grid; grid-template-columns: 1fr 1fr; flex: 1; min-height: 0; }
.notes-modal-edit { position: relative; min-height: 0; border-right: 1px solid var(--border-primary); }
.notes-modal-area { width: 100%; height: 100%; box-sizing: border-box; resize: none; padding: 0.75rem; border: none; background: transparent; color: var(--text-primary); font-family: var(--font-mono, monospace); font-size: var(--font-size-small); line-height: 1.6; }
.notes-modal-area:focus-visible { outline: none; }
.notes-modal-preview { padding: 0.75rem; overflow-y: auto; }
@media (max-width: 43.75rem) {
    .notes-modal { width: 100vw; height: 100vh; border-radius: 0; }
    .notes-modal-tabs { display: flex; gap: 0.25rem; padding: 0.25rem 0.75rem; border-bottom: 1px solid var(--border-primary); }
    .notes-modal-body { grid-template-columns: 1fr; }
    .notes-modal[data-tab="edit"] .notes-modal-preview, .notes-modal[data-tab="preview"] .notes-modal-edit { display: none; }
}

/* The health widget: a figure and what it counts, per row. */
.dashboard-widget-health {
    display: flex;
    flex-direction: column;
    /* The hairline between rows is the gap showing through -- see
       .dashboard-widget-stats, which is bound the same way and for the same
       reason. */
    gap: 1px;
    padding: 1px;
    border-radius: var(--radius-4, 7px);
    overflow: hidden;
    background: var(--dashboard-widget-rule);
}

.dashboard-widget-health-row {
    display: flex;
    align-items: baseline;
    gap: var(--space-3, 0.65rem);
    width: 100%;
    padding: var(--space-1-5, 0.34rem) var(--space-2, 0.55rem);
    border: none;
    background: var(--dashboard-widget-cell);
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.dashboard-widget-health-row:hover,
.dashboard-widget-health-row:focus-visible {
    background: var(--dashboard-widget-cell-hover);
}

/*
 * The figure at the size the figures on every other tile are set.
 *
 * This is a list rather than a grid because each row is a filter to click, but
 * it is the same instrument: a number that carries, and a word that explains
 * it. Sized and tracked to match .dashboard-widget-stat-value so a dashboard
 * holding health beside archive reads as one product.
 */
.dashboard-widget-health-value {
    min-width: 2.4em;
    font-size: 1.35em;
    line-height: 1.05;
    font-weight: var(--font-weight-semibold);
    letter-spacing: -0.015em;
    /* Figures read as a column when their digits line up. */
    font-variant-numeric: tabular-nums;
}

/*
 * Tone by what the figure means, not by its size.
 *
 * A zero in the broken row is the best news on the widget and still reads as
 * the broken row -- colouring by value would make "0 broken" green and
 * indistinguishable from "12 healthy".
 */
.dashboard-widget-health-row--bad .dashboard-widget-health-value { color: var(--accent-error); }
.dashboard-widget-health-row--warn .dashboard-widget-health-value { color: var(--accent-warning); }
.dashboard-widget-health-row--good .dashboard-widget-health-value { color: var(--accent-success); }

.dashboard-widget-health-label {
    color: var(--text-tertiary);
    font-size: var(--font-size-small);
    letter-spacing: 0.03em;
}
.dashboard-widget-health-waiting { margin: 0; color: var(--text-tertiary); font-style: italic; }

.config-widget-kind {
    font-size: var(--font-size-small);
    color: var(--text-tertiary);
    margin-left: 8px;
}

/* ─── Widget rows ────────────────────────────────────────────────────────── */
/* Shared by the widgets that list rows — sources, feeds, certificates. One set
   of rules rather than three near-copies: they differ in what they say, not in
   how a row looks. */
.dashboard-widget-rows {
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.dashboard-widget-row {
    display: flex;
    align-items: baseline;
    /*
     * The name takes the slack, so the figure beside it does not.
     *
     * space-between put the gap wherever the name stopped, which left the
     * numbers down the column starting at four different places -- barely
     * visible on a narrow tile and plainly crooked on a wide one, since a
     * wider tile gives the names more room to differ. Measured at 640px the
     * spread was 63px, and the rule below this one already promised these were
     * "lined up down the column".
     */
    gap: 10px;
    width: 100%;
    /* Taller than it was: these rows sit under a block of figures now, and a
       three-pixel row against a 1.5em number reads as an afterthought. It is
       also a click target on most of the tiles that use it. */
    padding: var(--space-1, 0.28rem) var(--space-2, 0.45rem);
    border: 0;
    border-radius: var(--layout-radius-sm, 5px);
    background: transparent;
    color: inherit;
    font: inherit;
    text-align: left;
}

button.dashboard-widget-row {
    cursor: pointer;
}

button.dashboard-widget-row:hover,
button.dashboard-widget-row:focus-visible,
button.dashboard-widget-row.keyboard-selected {
    background: rgba(127, 127, 127, 0.09);
}

/* The cursor has to read as "here" even without :focus-visible, which browsers
   withhold after a click — the same reason the show-more toggle carries a ring
   of its own. Inside a widget it also has to stand apart from the hover above,
   since a reader arrowing through a tile is often hovering something else. */
.dashboard-widget-body button.keyboard-selected {
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-primary) 45%, transparent);
    outline: none;
}

.dashboard-widget-row-name {
    /* min-width:0 or the ellipsis never happens: a flex item will not shrink
       below the width of its own text without it. */
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* The number or the verdict, kept on one line and lined up down the column.
   The width is a floor rather than a fixed size, so "100.0%" and "down" end at
   the same edge without either being clipped. */
.dashboard-widget-row-detail {
    flex: none;
    min-width: 3.4rem;
    text-align: right;
    font-variant-numeric: tabular-nums;
    color: var(--text-tertiary);
}

.dashboard-widget-row--bad .dashboard-widget-row-detail {
    color: var(--accent-error);
}

.dashboard-widget-row--warn .dashboard-widget-row-detail {
    color: var(--accent-warning);
}

/* Off is not a problem: a sleeping disk, a stopped VM. Grey, name and all. */
.dashboard-widget-row--off .dashboard-widget-row-name,
.dashboard-widget-row--off .dashboard-widget-row-detail {
    color: var(--text-tertiary);
}

/* A figure with its qualifier beside it — the inbox count and how old the
   oldest is, the neglected count and the threshold that produced it. The
   qualifier is load-bearing: "eleven neglected" is a different claim at thirty
   days than at a year. */
.dashboard-widget-headline {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 2px 9px;
    margin-bottom: 8px;
}

.dashboard-widget-headline-value {
    font-size: 1.75em;
    font-weight: var(--font-weight-semibold);
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}

/*
 * The qualifier, and it is load-bearing rather than decorative: "eleven
 * neglected" is a different claim at thirty days than at a year. Allowed to
 * wrap under the number on a narrow tile instead of being clipped -- a
 * half-shown condition is worse than a second line.
 */
.dashboard-widget-headline-note {
    color: var(--text-tertiary);
    font-size: var(--font-size-small);
    line-height: 1.4;
    text-wrap: pretty;
}

/* The heartbeat, as a row of ticks rather than a chart: what it has to show is
   a pattern of good and bad, not a value per moment. */
.dashboard-widget-spark {
    display: flex;
    flex: none;
    gap: 1px;
    align-items: flex-end;
    margin-left: 8px;
}

.dashboard-widget-spark-tick {
    width: 2px;
    height: 10px;
    border-radius: var(--radius-1, 1px);
    background: var(--border-secondary);
}

.dashboard-widget-spark-tick--up {
    background: var(--accent-success);
}

.dashboard-widget-spark-tick--down {
    background: var(--accent-error);
}

/* degraded, as health_uptime.go names it: a bucket where some samples answered
   and some did not. */
.dashboard-widget-spark-tick--degraded {
    background: var(--accent-warning);
}

.dashboard-widget-trend {
    display: flex;
    flex-direction: column;
    /* The health view's summary block, as a tile: label left, figure right,
       and the line and its sentence on a row of their own. Hairlines between
       rows, as .dashboard-widget-health draws them. */
    gap: 1px;
    padding: 1px;
    border-radius: var(--radius-4, 7px);
    overflow: hidden;
    background: var(--dashboard-widget-rule);
}

.dashboard-widget-trend-row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 2px var(--space-2, 0.55rem);
    width: 100%;
    padding: var(--space-1-5, 0.34rem) var(--space-2, 0.55rem);
    border: none;
    background: var(--dashboard-widget-cell);
    color: inherit;
    font: inherit;
    text-align: left;
}

button.dashboard-widget-trend-row {
    cursor: pointer;
}

button.dashboard-widget-trend-row:hover,
button.dashboard-widget-trend-row:focus-visible {
    background: var(--dashboard-widget-cell-hover);
}

.dashboard-widget-trend-label {
    color: var(--text-secondary);
}

.dashboard-widget-trend-value {
    color: var(--text-primary);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.dashboard-widget-trend-row.is-good .dashboard-widget-trend-value { color: var(--accent-success); }
.dashboard-widget-trend-row.is-warn .dashboard-widget-trend-value { color: var(--accent-warning); }
.dashboard-widget-trend-row.is-bad .dashboard-widget-trend-value { color: var(--accent-error); }

.dashboard-widget-trend-change {
    font-size: var(--font-size-small);
    font-variant-numeric: tabular-nums;
}

.dashboard-widget-trend-extra {
    flex-basis: 100%;
}

/* The rail's sparkline: same currentColor line, same fixed axis. */
.dashboard-widget-trend-line {
    display: block;
    width: 100%;
    height: 2rem;
    margin-top: 4px;
    color: var(--accent-success);
    opacity: 0.85;
}

.dashboard-widget-trend-note {
    margin: 2px 0 0;
    font-size: var(--font-size-small);
    color: var(--text-tertiary);
}

a.dashboard-widget-row {
    text-decoration: none;
}

a.dashboard-widget-row:hover,
a.dashboard-widget-row:focus-visible {
    background: rgba(127, 127, 127, 0.09);
}

/* The row that says what did not fit. Quieter than the rows above it — it is a
   pointer, not a finding — but present, because a list that cuts itself off in
   silence answers a different question than the one it appears to answer. */
.dashboard-widget-row--more .dashboard-widget-row-name {
    color: var(--text-tertiary);
    font-size: var(--font-size-small);
}

button.dashboard-widget-row--more {
    cursor: pointer;
}

/*
 * What a tile only says when it was drawn wide.
 *
 * Two columns should be more of the reading, not the same reading stretched.
 * The extra blocks are always built and hidden by width, so the tile answers
 * to the space it actually got: the grid narrows a two-column widget back to
 * one whenever the dashboard is showing one, and a phone never shows two at
 * all. Doing it in CSS rather than in the render is what makes dragging the
 * window narrower take effect at once, with nothing refetched.
 *
 * Two classes deep so it beats the single-class rules that give these blocks
 * their own display.
 */
@container (max-width: 27.99rem) {
    .dashboard-widget-panel .dashboard-widget-wide-only {
        display: none;
    }
}

/*
 * A row that gains a second detail keeps its name readable.
 *
 * The name takes the slack and shrinks to nothing without a floor: paired into
 * two columns, with a figure and a date after it, the title of a calendar
 * entry was squeezed to nought pixels and the row read as two numbers with no
 * subject. A floor is enough -- the ellipsis still does the rest.
 */
.dashboard-widget-panel .dashboard-widget-row-name {
    min-width: 4.5rem;
}

.dashboard-widget-panel .dashboard-widget-row-detail.dashboard-widget-wide-only {
    flex: 0 1 auto;
    min-width: 0;
    /* One line, cut if it must be: a date that wrapped turned every row into
       three, and a list of three-line rows is not the list the reader came
       for. */
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* And the other way: what stands in for it while the tile is narrow. */
@container (min-width: 27.99rem) {
    .dashboard-widget-panel .dashboard-widget-narrow-only {
        display: none;
    }
}

/*
 * The trend rows in two files once the tile is wide.
 *
 * The score and the trend keep the whole width: the line under the trend is
 * drawn to be read across the tile, and halving it halves the only picture
 * this widget draws.
 */
@container (min-width: 28rem) {
    .dashboard-widget-panel .dashboard-widget-trend {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .dashboard-widget-panel .dashboard-widget-trend-row[data-trend-row="score"],
    .dashboard-widget-panel .dashboard-widget-trend-row[data-trend-row="trend"] {
        grid-column: 1 / -1;
    }
}

/* The health figures: a column when narrow, two abreast once there is room. */
@container (min-width: 22rem) {
    .dashboard-widget-panel .dashboard-widget-health {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

/* ─── Kept widget ────────────────────────────────────────────────────────── */
/* Which order the tile is showing, and the shuffle for the one order that has
   anything to shuffle. Above the rows rather than in the widget's title: the
   title says what the tile is, and this says which five of forty it chose. */
.dashboard-widget-kept-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2, 0.5rem);
    padding-bottom: 3px;
    border-bottom: 1px solid var(--dashboard-widget-rule);
}

.dashboard-widget-kept-sort {
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    color: var(--text-tertiary);
    font-size: var(--font-size-small);
}

.dashboard-widget-kept-shuffle {
    flex: none;
    padding: 0 5px;
    border: 0;
    border-radius: var(--radius-4, 7px);
    background: none;
    color: var(--text-tertiary);
    font: inherit;
    line-height: 1.4;
    cursor: pointer;
}

.dashboard-widget-kept-shuffle:hover,
.dashboard-widget-kept-shuffle:focus-visible {
    background: rgba(127, 127, 127, 0.14);
    color: var(--text-secondary);
}

/* A tag heading takes the whole width: two columns of groups would have to be
   balanced by height, and a heading at the foot of one column with its rows at
   the head of the next is worse than no grouping at all. */
.dashboard-widget-kept-group {
    grid-column: 1 / -1;
    margin: var(--space-2, 0.45rem) 0 0;
    padding: 0 var(--space-2, 0.45rem);
    color: var(--text-tertiary);
    font-size: var(--font-size-small);
}

.dashboard-widget-rows--kept .dashboard-widget-kept-group:first-child {
    margin-top: 0;
}

/* The name over its address, so the row stays one click target. Two lines is
   the whole reason for the wrapper: the row itself lines its children up on a
   baseline, which a stacked pair must not do. */
.dashboard-widget-row--kept {
    align-items: center;
}

.dashboard-widget-row-text {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
    flex: 1 1 auto;
    text-align: left;
}

/* Clamped by the column and not only by the character count in the script: a
   narrow tile is narrower than any number of characters. */
.dashboard-widget-row-url {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    color: var(--text-tertiary);
    font-size: calc(var(--font-size-small) * 0.9);
    line-height: 1.3;
}

/* The custom widget's figures: a value with its name under it, side by side.
   Two or three numbers is the common case, so they wrap rather than scroll. */
.dashboard-widget-figures {
    display: flex;
    flex-wrap: wrap;
    gap: 12px 20px;
}

.dashboard-widget-figure {
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.dashboard-widget-figure-value {
    font-size: 1.4em;
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
}

.dashboard-widget-figure-label {
    color: var(--text-tertiary);
    font-size: var(--font-size-small);
}

/* A path that stopped matching is marked rather than blank — a blank reads as a
   zero, and zero is a fact. */
.dashboard-widget-figure.is-missing .dashboard-widget-figure-value {
    color: var(--text-tertiary);
}

/*
 * Four shapes, so a tile of figures is not a list the reader has to weigh.
 *
 * Size carries the ranking and nothing else does: no borders, no boxes, no
 * second colour. A figure that matters is bigger than one that gives it
 * context, which is the whole of what the reader needs to be told and is
 * legible on every theme, because it asks the palette for nothing.
 */
.dashboard-widget-figure--large .dashboard-widget-figure-value {
    font-size: 2.1em;
    font-weight: 600;
    letter-spacing: -0.01em;
}

.dashboard-widget-figure--small .dashboard-widget-figure-value {
    font-size: 1em;
    color: var(--text-secondary);
}

/* A meter needs its own column: the bar belongs under the number it measures,
   at the width of the figure rather than of the tile. */
.dashboard-widget-figure--meter {
    flex: 1 1 8rem;
    min-width: 6rem;
}

.dashboard-widget-figure--meter .dashboard-widget-meter {
    margin-top: 6px;
}

/*
 * Wide enough to put the headline figure on its own line.
 *
 * The same breakpoint the panel's other layouts use, and for the same reason:
 * it answers to the width the tile was given, not to the columns the dashboard
 * was asked for. A widget set to two columns that the grid narrowed back to one
 * never reaches this, which is the point -- the figures stay stacked rather
 * than being spread across a width that is not there.
 */
@container (min-width: 22rem) {
    .dashboard-widget-panel .dashboard-widget-figures {
        gap: 14px 26px;
    }

    .dashboard-widget-panel .dashboard-widget-figure--large {
        flex-basis: 100%;
    }
}

.dashboard-widget-asof {
    margin: 8px 0 0;
    color: var(--text-tertiary);
    font-size: calc(var(--font-size-small) * 0.92);
}

/* The weather widget: current conditions stacked above the forecast, side by
   side once the tile is wide enough (see the panel container query below). */
.dashboard-widget-weather {
    display: flex;
    flex-direction: column;
    gap: var(--space-3, 0.75rem);
}

.dashboard-widget-weather-current {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
}

.dashboard-widget-weather-current .weather-icon-svg {
    width: 1.8em;
    height: 1.8em;
}

.dashboard-widget-weather-temp {
    font-size: 2.1em;
    font-weight: 600;
    line-height: 1.1;
    letter-spacing: -0.01em;
    font-variant-numeric: tabular-nums;
}

.dashboard-widget-weather-condition,
.dashboard-widget-weather-location {
    color: var(--text-tertiary);
    font-size: var(--font-size-small);
}

.dashboard-widget-weather-forecast {
    display: flex;
    flex-direction: column;
    gap: 1px;
    border-radius: var(--radius-4, 7px);
    overflow: hidden;
    background: var(--dashboard-widget-rule);
}

.dashboard-widget-weather-row {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    padding: var(--space-1-5, 0.34rem) var(--space-2, 0.55rem);
    background: var(--dashboard-widget-cell);
    font-size: var(--font-size-small);
    font-variant-numeric: tabular-nums;
}

.dashboard-widget-weather-row-label {
    flex: 1 1 auto;
    color: var(--text-secondary);
}

.dashboard-widget-weather-row-range {
    color: var(--text-primary);
}

@container (min-width: 22rem) {
    .dashboard-widget-panel .dashboard-widget-weather {
        flex-direction: row;
        align-items: flex-start;
    }

    .dashboard-widget-panel .dashboard-widget-weather-current {
        flex: 0 0 auto;
        min-width: 6rem;
    }

    .dashboard-widget-panel .dashboard-widget-weather-forecast {
        flex: 1 1 auto;
    }
}

/* ─── Widgets that answer to their own width ─────────────────────────────── */
/*
 * A widget is drawn one or two columns wide, and `widgetColumnSpan` narrows a
 * two-column widget back to one whenever the dashboard is only showing one --
 * so config.columns records what was asked for and never what was given. A
 * container query reads the width that actually happened, which is also the
 * right answer on a phone, where the width setting cannot be reached at all.
 *
 * Scoped to this element rather than to .dashboard-widget-body: the nine tiles
 * that shipped before this were laid out in a plain block and should stay in
 * one.
 */
.dashboard-widget-panel {
    container-type: inline-size;
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 0.5rem);
}

/*
 * The figures: two abreast when narrow, four when there is room.
 *
 * A grid rather than a wrapping flex row, so the labels line up in columns
 * instead of drifting with the width of the number above them -- four figures
 * that do not share a baseline read as four unrelated facts.
 */
.dashboard-widget-stats {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    /*
     * Bound as one readout rather than spaced as four facts.
     *
     * The figures sat in open space with a generous gap, and four numbers
     * floating on a card read as four unrelated things -- which on a tile whose
     * whole job is a comparison ("0 kept against 4 lost") is the one reading
     * that must not happen. An inset surface with hairlines between the cells
     * says they belong to each other, and it separates the figures from the
     * list of rows that several of these tiles put underneath.
     */
    gap: 1px;
    padding: 1px;
    border-radius: var(--radius-4, 7px);
    overflow: hidden;
    background: var(--dashboard-widget-rule);
}

/*
 * Four abreast once there is room for it.
 *
 * Answering to the container and not the viewport: a widget is drawn one or
 * two columns wide and narrowed back to one whenever the dashboard is showing
 * one, so the width that matters is the width this tile actually got.
 */
@container (min-width: 22rem) {
    .dashboard-widget-stats--wide {
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }
}

/*
 * Each cell paints the tile's own ground, so the 1px grid gap behind them is
 * what draws the hairlines -- one background instead of eight borders that
 * would double up wherever two cells meet.
 */
.dashboard-widget-stat {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
    padding: var(--space-2, 0.45rem) var(--space-2, 0.55rem);
    border: 0;
    background: var(--dashboard-widget-cell);
    color: inherit;
    font: inherit;
    text-align: left;
}

button.dashboard-widget-stat {
    cursor: pointer;
}

/*
 * One rule, not two. A second block followed this one at the same specificity
 * and mixed a grey into --background-primary, so it won for hover and focus
 * and left the accent tint reachable only by keyboard -- the same cell lit up
 * in two different colours depending on how you got to it.
 */
button.dashboard-widget-stat:hover,
button.dashboard-widget-stat:focus-visible,
button.dashboard-widget-stat.keyboard-selected {
    background: color-mix(in srgb, var(--accent-primary) 8%, var(--dashboard-widget-cell));
}

/*
 * The figure, and the whole point of a tile.
 *
 * Large, tight and lined up: a dashboard is scanned rather than read, so the
 * number has to carry at a glance and the word under it only has to survive a
 * second look. These were within a hair of the same size before, which is what
 * made a tile read as a paragraph of digits.
 */
.dashboard-widget-stat-value {
    font-size: 1.5em;
    line-height: 1.05;
    font-weight: var(--font-weight-semibold);
    letter-spacing: -0.015em;
    font-variant-numeric: tabular-nums;
    /* Long values -- a size, a version, a date -- shrink rather than push the
       cell beside them out of the grid. */
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.dashboard-widget-stat-label {
    color: var(--text-tertiary);
    font-size: var(--font-size-small);
    /* Tracked out a little: a short dim word under a large number needs the
       width to read as a caption rather than as a smudge. */
    letter-spacing: 0.03em;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* Tone by what the figure means, never by how large it is -- the same rule the
   health widget follows, and for the same reason: nought lost is the best news
   the tile can carry and still belongs in the lost cell. */
.dashboard-widget-stat--bad .dashboard-widget-stat-value { color: var(--accent-error); }
.dashboard-widget-stat--warn .dashboard-widget-stat-value { color: var(--accent-warning); }
.dashboard-widget-stat--good .dashboard-widget-stat-value { color: var(--accent-success); }

/*
 * A nought is not an alarm.
 *
 * Tone says which figure this is; it should not also shout when the figure is
 * the good one. Every value used to be painted its row's colour, so a tile
 * reading "0 lost, 0 died, 4 broken" was three reds and the reader had to
 * check all three to find the one that wanted them. Dimming the noughts leaves
 * exactly the numbers that need someone coloured, and the label still says
 * which row it is -- so "0 broken" is still plainly the broken figure.
 *
 * Every tone, the good one included. Painting "0 healthy" in the success
 * colour was the worst case of all: green is the screen's word for "this is
 * fine", and no healthy links is not fine. A nought is dim whatever row it is
 * in, and the colours are left to mean what they say.
 */
.dashboard-widget-stat.is-quiet .dashboard-widget-stat-value,
.dashboard-widget-health-row.is-quiet .dashboard-widget-health-value {
    color: var(--text-tertiary);
    font-weight: var(--font-weight-normal, 400);
}

/*
 * A proportion as a bar, beside the figure and never instead of it.
 *
 * It says how full at a glance and cannot say of what; "42 of 118" is the part
 * a person repeats to themselves afterwards.
 */
.dashboard-widget-meter {
    height: 6px;
    border-radius: var(--radius-2, 3px);
    overflow: hidden;
    /* A track that can be seen when the fill is short: at four pixels on a
       16%-tint ground the empty part of the bar was invisible, so a bar at 5%
       looked like a stray mark rather than like a bar that is nearly empty.
       Solid on the cell's ground, and a lighter share of the ink: at 22% over
       transparent it was the brightest thing on the tile. */
    background: color-mix(in srgb, var(--text-primary) 14%, var(--dashboard-widget-cell));
}

.dashboard-widget-meter-fill {
    display: block;
    height: 100%;
    border-radius: inherit;
    transition: width 240ms ease;
}

.dashboard-widget-meter-fill--good { background: var(--accent-success); }
.dashboard-widget-meter-fill--warn { background: var(--accent-warning); }
.dashboard-widget-meter-fill--bad { background: var(--accent-error); }

/* What the tile assumed, or what happens next. Quieter than a row, because it
   is a condition on the figures rather than one of them. */
.dashboard-widget-footnote {
    margin: 2px 0 0;
    color: var(--text-tertiary);
    font-size: var(--font-size-small);
}

.dashboard-widget-footnote--bad { color: var(--accent-error); }
.dashboard-widget-footnote--warn { color: var(--accent-warning); }
.dashboard-widget-footnote--good { color: var(--accent-success); }

/* The container list's count sits above its rows rather than closing them. */
.dashboard-widget-footnote.dashboard-widget-containers-count {
    margin: 0 0 4px;
}

.dashboard-widget-panel .dashboard-widget-headline {
    margin-bottom: 0;
}

/*
 * Where the two widths actually fall, since the numbers are not arbitrary.
 *
 * A grid column is bounded at 250px..360px (--dashboard-column-min/max), and
 * the body takes 1.7rem of padding plus its border off that -- so a one-column
 * widget's panel is 221px..331px, and a two-column one is that doubled plus a
 * 1.5rem gap: 487px at its narrowest, on a wide screen carrying six columns.
 *
 * 24rem and 28rem therefore sit in the gap between the two, with room on both
 * sides: no one-column widget can reach them and no two-column widget can miss
 * them. rem is safe here because nothing in this app changes the root size --
 * the font-size setting scales its own variables against a fixed root.
 */
@container (min-width: 24rem) {
    .dashboard-widget-stats {
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }

    /*
     * ...except where four does not divide the figures.
     *
     * A widget whose figures the reader chooses can hand this any number, and
     * four columns under six cells leaves two patches of empty ground, while
     * two cells fill half a row. These are the counts where a different number
     * reads better; everything else keeps the four above.
     */
    .dashboard-widget-stats[data-stat-count="2"] {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .dashboard-widget-stats[data-stat-count="3"],
    .dashboard-widget-stats[data-stat-count="6"],
    .dashboard-widget-stats[data-stat-count="9"] {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }

    .dashboard-widget-stats[data-stat-count="5"] {
        grid-template-columns: repeat(5, minmax(0, 1fr));
    }
}

/*
 * The rows, paired when the tile is wide.
 *
 * The second column of a two-column widget was otherwise white space beside a
 * single file of short rows, and a row is a name with a figure at the end --
 * exactly the shape that pairs without becoming a table.
 */
@container (min-width: 28rem) {
    .dashboard-widget-rows--pairs {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 2px var(--space-5, 1.4rem);
        align-content: start;
    }

    /* The count of what did not fit belongs to the whole list, not to whichever
       column it happened to land in. */
    .dashboard-widget-rows--pairs .dashboard-widget-row--more {
        grid-column: 1 / -1;
    }
}

/*
 * Unraid: rows that read down, not across.
 *
 * The overview and the disk list are sequences -- parity, then the array in
 * disk order, then cache -- and pairing them row by row would read disk1 beside
 * disk2. Wide, they flow down two columns instead, each group head staying
 * with the disk under it.
 */
@container (min-width: 28rem) {
    .dashboard-widget-panel .unraid-columns {
        display: block;
        column-count: 2;
        column-gap: var(--space-5, 1.4rem);
    }

    .dashboard-widget-panel .unraid-columns > * {
        break-inside: avoid;
    }

    .dashboard-widget-panel .unraid-columns > .dashboard-widget-group {
        break-after: avoid;
    }
}

/* Unraid array rows: a fill bar between the disk name and its reading. */
.unraid-disk-row { gap: var(--space-2, 0.5rem); }
/* The shares that live on the place the row above names. */
.unraid-share-names {
    margin: 0 0 var(--space-1, 0.25rem);
    padding: 0 var(--space-1, 0.25rem);
    color: var(--text-tertiary);
    font-size: var(--text-xs, 0.72rem);
    line-height: 1.35;
    overflow-wrap: anywhere;
}
/* Wide enough for "58% · 3 errors", so the bars line up down the column. */
.unraid-disk-row .dashboard-widget-row-detail { min-width: 7.5rem; }
/* A narrow tile shows the bare percentage; 7.5rem left no room for the bar. */
@container (max-width: 27.99rem) {
    .unraid-disk-row .dashboard-widget-row-detail { min-width: 3.5rem; }
}
.unraid-disk-bar {
    /* Gives way before the figure does: on a one-column tile the figure was
       pushed past the edge and cut off. */
    flex: 0 10 4rem;
    min-width: 1.5rem;
    align-self: center;
    height: 4px;
    border-radius: 2px;
    background: linear-gradient(90deg, var(--accent-primary) var(--fill), var(--border-secondary) var(--fill));
}
.unraid-disk-bar--warn {
    background: linear-gradient(90deg, var(--accent-warning) var(--fill), var(--border-secondary) var(--fill));
}
/* The UPS figure on battery: amber, as the bar under it is. */
.dashboard-widget-headline-value--warn { color: var(--accent-warning); }
.dashboard-widget-group {
    margin: var(--space-2, 0.5rem) 0 var(--space-1, 0.25rem);
    /* The rows' own inset, so a group head starts where its disk names do. */
    padding: 0 var(--space-2, 0.45rem) 2px;
    border-bottom: 1px solid var(--border-secondary);
    color: var(--text-tertiary);
    font-size: var(--text-xs, 0.7rem);
    text-transform: lowercase;
    letter-spacing: 0.06em;
}
.dashboard-widget-group:first-child { margin-top: 0; }

/*
 * A figure older than the interval it refreshes on.
 *
 * Not an error -- the number is what it was -- but the reader was led to
 * expect it to have moved by now, and a cached figure that looks live is
 * worse than a stale one that admits it.
 */
.dashboard-widget-asof--stale {
    color: var(--accent-warning);
}

/*
 * The delta beside a widget figure, and the key in its corner.
 *
 * The delta is toned by what the change means, not by its sign: one more
 * broken bookmark is worse, one more reachable one is better, and the widget
 * is the only thing that knows which it is holding.
 */
.dashboard-widget-stat-delta {
    margin-left: var(--space-1, 0.25rem);
    font-size: var(--text-2xs, 0.65rem);
    font-weight: var(--font-weight-semibold);
    font-variant-numeric: tabular-nums;
    color: var(--text-tertiary);
}

/*
 * Both names, one set of tones.
 *
 * The trend tile grew its own is-better / is-worse / is-level before the
 * shared delta existed, and the two agreed only by coincidence. Listed
 * together, a change to what "better" looks like cannot land in one and miss
 * the other. The trend tile keeps its class name because its layout rules
 * select on it.
 */
.dashboard-widget-stat-delta--good,
.dashboard-widget-trend-change.is-better {
    color: var(--accent-success);
}

.dashboard-widget-stat-delta--warn {
    color: var(--accent-warning);
}

.dashboard-widget-stat-delta--bad,
.dashboard-widget-trend-change.is-worse {
    color: var(--accent-error);
}

.dashboard-widget-trend-change.is-level {
    color: var(--text-tertiary);
}

/*
 * Waits until the reader is on the tile. A grid of figures each shouting its
 * key would be louder than the figures, and the key is only useful to someone
 * already looking at that one.
 */
.dashboard-widget-stat-key {
    position: absolute;
    top: 2px;
    right: 3px;
    padding: 0;
    border: none;
    background: none;
    font-family: inherit;
    font-size: var(--text-2xs, 0.65rem);
    line-height: 1;
    color: var(--text-tertiary);
    opacity: 0;
    transition: opacity 0.16s ease;
}

.dashboard-widget-stat {
    position: relative;
}

.dashboard-widget-stat:is(:hover, :focus-visible) .dashboard-widget-stat-key {
    opacity: 0.75;
}

body.no-animations .dashboard-widget-stat-key {
    transition: none;
}

/* ══════════════════════════════════════════════════════════════════════════
   The header band — the three-zone design, as drawn.

   Taken from the Superdesign draft "nextDash — Responsive Header Evolution"
   at 1500px: one slab holding three zones, left to right. The name and the
   clock on the left in a fixed column; the pages in the middle, centred,
   between the two keys that walk them; what you do and where you go on the
   right, the actions boxed together and the destinations standing free.

   The numbers below are the draft's own: 0.6rem/1.25rem of padding, 28px
   controls, 4px corners inside an 8px slab, and a page tab that carries its
   key as a superscript.
   ══════════════════════════════════════════════════════════════════════════ */

/*
 * The bar runs the width of the window; its contents run the width of the page.
 *
 * As a card it was an island: 1309px of slab with an 8px corner and a cast,
 * floating over content 1040px wide that carries no card of its own -- and in
 * config, inbox and health a second card started 24px below it, so the top of
 * every view was two loose plates. Edge to edge with one hairline under it, it
 * is chrome rather than a card, and there is no rim left to misalign.
 *
 * What still has to line up is what is *in* it: the name sits on the same
 * vertical as the first column of the grid, and in a view on the same vertical
 * as the panel. That is --header-column, set per view below.
 */
.dashboard-section.section-controls {
    /*
     * No fill of its own: the page's background runs straight through.
     *
     * A slab here was a second surface over the theme's own -- pattern,
     * gradient and tint all stopped at the top of the page and started again
     * below the bar. One hairline is enough to say where the chrome ends, and
     * the controls carry their own plates.
     */
    background: transparent;
    /*
     * No rule under it either.
     *
     * The hairline was what said where the chrome ended while the bar was a
     * slab over the page. With no fill and no card there is nothing to close
     * off, and a line the full width of the window is the loudest thing on a
     * page whose own content draws none. The gap below is unchanged: it takes
     * over the pixel the rule used to occupy.
     */
    border-bottom: 0;
    /* The air under the header belongs to the page, not to the bar: as padding
       it was inside the fill and the hairline sat 24px below the controls. */
    padding-bottom: 0;
    /*
     * The page used to breathe here.
     *
     * The band that held the view's name stood between the bar and the grid,
     * so 60px of nothing separated the chrome from the first category. Folding
     * the name into the header row took the band away and the air with it, and
     * the columns ended up hard against the controls. The air is the point,
     * not the band: it is measured out here instead.
     */
    margin-bottom: calc(var(--space-7, 3rem) + var(--space-3, 0.75rem));
}

.dashboard-section.section-controls .header-top {
    display: grid;
    /*
     * The left zone is a width, not a measurement of what stands in it.
     *
     * At `auto` the column followed the identity, and the identity is three
     * different widths: "config" against "main", the clock beside the name
     * against the clock in a column of its own. The pages moved sideways with
     * every one of them. One width for the zone, whatever it holds, and the
     * strip stays where the reader left it.
     */
    --header-identity-width: 28rem;
    /*
     * Three columns, and the two outer ones are equal by construction: the
     * strip in the middle therefore stands on the centre of the row whatever
     * is beside it. A fixed left column held it still but not central -- the
     * actions on the right are narrower than the identity on the left, so the
     * free space between them was not centred on the page.
     *
     * The outer columns take 0 as their floor: neither may push the other off
     * the row, and both cut their own content instead.
     */
    grid-template-columns: minmax(0, 1fr) minmax(0, auto) minmax(0, 1fr);
    align-items: center;
    gap: var(--space-4, 1rem);
    /* 10px rather than the draft's 9.6: at 0.6rem the row landed on a half
       pixel, and the hairline under every control in it was the half that got
       rounded away. */
    padding: 0.625rem 0;
    /* The page's own column, and centred in the bar the way the page is
       centred on the screen. */
    width: 100%;
    max-width: var(--header-column, none);
    margin-inline: auto;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
}

/*
 * One header, one position, whatever view is open.
 *
 * It followed each view's own column for a while -- 1040px in config, 68rem in
 * the list views -- which meant the add button and the config icon moved as
 * you walked between them: the same control in three places depending on where
 * you had been. The dashboard is the one that sets it, and every view loads
 * the header on those positions. --header-column stays unset, so the row keeps
 * the page container's width.
 */

/* ── Left: the name, and the line under it ────────────────────────────────── */

.dashboard-section.section-controls .header-identity {
    /* Wide enough for the name and the clock beside it, and fixed so the
       middle zone starts in the same place whatever the page is called. */
    flex: 0 0 auto;
    gap: 0;
}

.dashboard-section.section-controls .header-identity .title {
    /* One line, cut rather than wrapped: a long page name wrapped into the
       clock beside it and took the row to two lines -- the one thing this
       header is built not to do. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
    font-size: 1.5rem;
    font-weight: 900;
    line-height: 1;
    letter-spacing: -0.05em;
    opacity: 1;
}

.dashboard-section.section-controls .header-identity .title-breadcrumb {
    font-size: 0.65rem;
    margin-top: 2px;
}

/*
 * The clock reads as text, not as a footnote.
 *
 * It was 0.65rem of --text-tertiary at 60% opacity -- the smallest type on the
 * page in its faintest colour, which on a light theme is barely there at all.
 * The time now sits beside the name at the reader's own size and the weather
 * runs under it, both at full strength.
 *
 * Laid out with `display: contents` on the wrappers so the two lines
 * renderDateWeatherLine builds can be placed separately without rewriting what
 * either of them says -- the popover and the error state still hang off them.
 */
.dashboard-section.section-controls .header-identity {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: baseline;
    column-gap: var(--space-2, 0.6rem);
    row-gap: 2px;
    /* As wide as it needs up to its own ceiling, and never wider than the
       column it stands in. */
    width: min(var(--header-identity-width), 100%);
    justify-self: start;
}

.dashboard-section.section-controls .header-identity .title-wrapper {
    grid-column: 1;
    grid-row: 1;
}

.dashboard-section.section-controls .header-identity .header-top-primary,
.dashboard-section.section-controls .header-identity .date {
    display: contents;
}

.dashboard-section.section-controls .header-identity .date-time-line {
    grid-column: 2;
    grid-row: 1;
    font-size: calc(var(--font-size-text) * 1.14);
    font-weight: var(--font-weight-semibold, 600);
    line-height: 1.2;
    color: var(--text-secondary);
    white-space: nowrap;
}

.dashboard-section.section-controls .header-identity .date-weather-line {
    grid-column: 1 / -1;
    grid-row: 2;
    font-size: var(--font-size-text);
    font-weight: 500;
    line-height: 1.3;
    color: var(--text-secondary);
}

/* Everything else the block carries -- the mini status, the tag chip, the
   "checking status" line -- keeps its own row under the two. */
.dashboard-section.section-controls .header-identity #dashboard-mini-status,
.dashboard-section.section-controls .header-identity #tag-filter-indicator,
.dashboard-section.section-controls .header-identity #status-loading-indicator {
    grid-column: 1 / -1;
}

/*
 * Or a zone of its own, between the name and the pages.
 *
 * Same two lines, more room: the clock takes a column beside the name rather
 * than sharing its 18rem, which is what a larger face needs.
 */
/*
 * The name keeps a fixed column, the clock takes what it needs.
 *
 * Left to `auto` the first column followed the view's name -- "config" is
 * shorter than "main" is longer than "health" -- so the pages moved 26px
 * sideways every time you switched view.
 */
/*
 * Classic: the clock on a line of its own, the name in large type under it.
 *
 * How the top of the page was drawn before the header became one row -- a
 * quiet line of clock and weather, then the view's name at --font-size-title
 * with the grid under it. Folding the name into the row made the header
 * shorter, which is what most of the work in this header was for; this is here
 * because the tall version is calmer to look at, and that is a preference
 * rather than a fact.
 *
 * The row keeps its three columns and gains a second line: the pages and the
 * destinations stay on the first, beside the clock, and the name spans the
 * full width underneath them.
 */
body[data-header-clock="classic"] .dashboard-section.section-controls .header-top {
    row-gap: var(--space-2, 0.5rem);
    align-items: start;
}

body[data-header-clock="classic"] .dashboard-section.section-controls .header-identity {
    display: contents;
}

/* First line: the clock at the left, the pages in the middle, the
   destinations at the right -- the three columns the row already has. The two
   lines of the clock stay one block, so the weather sits under the time rather
   than under the tallest thing on the row. */
body[data-header-clock="classic"] .dashboard-section.section-controls .header-top-primary {
    /* The default placement makes this `display: contents` so the two lines
       can be placed apart; here they belong together, so it is a box again. */
    display: block;
    grid-column: 1;
    grid-row: 1;
    min-width: 0;
}

body[data-header-clock="classic"] .dashboard-section.section-controls .header-identity .date {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
}

body[data-header-clock="classic"] .dashboard-section.section-controls .date-time-line {
    font-size: calc(var(--font-size-text) * 1.14);
    color: var(--text-secondary);
    white-space: nowrap;
}

body[data-header-clock="classic"] .dashboard-section.section-controls .date-weather-line {
    font-size: var(--font-size-text);
    color: var(--text-secondary);
    white-space: nowrap;
}

body[data-header-clock="classic"] .dashboard-section.section-controls .header-track,
body[data-header-clock="classic"] .dashboard-section.section-controls .header-actions {
    grid-row: 1;
}

/* Second line: the name, in the type it had, spanning the row. */
body[data-header-clock="classic"] .dashboard-section.section-controls .title-wrapper {
    grid-column: 1 / -1;
    grid-row: 2;
    min-width: 0;
}

body[data-header-clock="classic"] .dashboard-section.section-controls .title {
    /* Two thirds of the old title: the band it used to stand in is gone, so
       the name no longer has to fill one -- it only has to read as the name of
       the page, above a grid that starts right under it. */
    font-size: calc(var(--font-size-title, 3rem) * 0.66);
    font-weight: var(--font-weight-black, 900);
    line-height: 1.1;
    letter-spacing: -0.04em;
    opacity: 0.8;
}

/*
 * The name is already the air between the controls and the grid.
 *
 * The other two placements need the full gap under the bar because the row
 * ends at the controls; here a line of large type stands between them, so the
 * same gap on top of it left a hole.
 */
body[data-header-clock="classic"] .dashboard-section.section-controls {
    margin-bottom: var(--space-4, 1rem);
}

/* Everything else the block carries keeps its own line under the name. */
body[data-header-clock="classic"] .dashboard-section.section-controls #dashboard-mini-status,
body[data-header-clock="classic"] .dashboard-section.section-controls #tag-filter-indicator,
body[data-header-clock="classic"] .dashboard-section.section-controls #status-loading-indicator {
    grid-column: 1 / -1;
    grid-row: 3;
}

/*
 * The row keeps its three columns; the split happens inside the identity.
 *
 * As four columns on the row the clock widened the left of the header and the
 * strip moved with it. The name and the clock share the identity's own grid
 * now, so both placements hand the middle column the same edges.
 */
body[data-header-clock="own-zone"] .dashboard-section.section-controls .header-identity {
    grid-template-columns: 9rem minmax(0, 1fr);
    align-items: center;
}

/* Every zone may be cut; none may push its neighbour out of the row. */
body[data-header-clock="own-zone"] .dashboard-section.section-controls .title-wrapper,
body[data-header-clock="own-zone"] .dashboard-section.section-controls .header-top-primary {
    min-width: 0;
}

body[data-header-clock="own-zone"] .dashboard-section.section-controls .header-identity .header-top-primary,
body[data-header-clock="own-zone"] .dashboard-section.section-controls .header-identity .date {
    display: contents;
}

body[data-header-clock="own-zone"] .dashboard-section.section-controls .header-identity .title-wrapper {
    grid-column: 1;
}

body[data-header-clock="own-zone"] .dashboard-section.section-controls .header-identity .date-time-line {
    font-size: calc(var(--font-size-text) * 1.43);
    color: var(--text-primary);
    /* The zone is a width now: a long line is cut here rather than widening
       the column the strip starts after. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}

body[data-header-clock="own-zone"] .dashboard-section.section-controls .header-identity .date-weather-line {
    grid-column: 2;
    font-size: calc(var(--font-size-text) * 1.03);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}

/* The clock's own column: the two lines stack in it, apart from the name. */
body[data-header-clock="own-zone"] .dashboard-section.section-controls .header-identity .header-top-primary {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    grid-column: 2;
    padding-left: var(--space-3, 0.9rem);
    border-left: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
}

body[data-header-clock="own-zone"] .dashboard-section.section-controls .header-identity .date {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
}

/* ── Middle: the pages, between the keys that walk them ───────────────────── */

.header-track {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.75rem;
    min-width: 0;
}

/*
 * Shift+Arrow moves a page at a time, and has since long before anything said
 * so. Printed at either end of the track, quiet enough to ignore.
 */
.page-walk-hint {
    flex: 0 0 auto;
    /* A button now, walking one page; it was a printed hint, and the reset is
       what keeps it looking like one. */
    border: none;
    cursor: pointer;
    font: inherit;
    padding: 2px 4px;
    border-radius: 3px;
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
    color: var(--text-tertiary);
    font-family: var(--font-family-main);
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    line-height: 1;
    white-space: nowrap;
    opacity: 0.4;
    transition: opacity 0.15s ease, color 0.15s ease;
}

.page-walk-hint:hover:not(:disabled),
.page-walk-hint:focus-visible:not(:disabled) {
    opacity: 1;
    color: var(--text-primary);
}

/* At the first page, or the last: the keys wrap around, the buttons do not --
   one that looks pressable and does nothing is worse than one that says so. */
.page-walk-hint:disabled {
    opacity: 0.15;
    cursor: default;
}

/*
 * The track fills the middle zone rather than sizing to its tabs.
 *
 * fitPageTabs measures against this element's own width, so a content-sized
 * track reports "everything fits" however far past the header it runs -- the
 * last tab was clipped in half and no chip was drawn at all. Filling the zone
 * makes the measurement the honest one, and the tabs stay centred in it.
 */
.header-track .page-navigation {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    gap: 0.25rem;
    min-width: 0;
    overflow: hidden;
    /* Sized by its tabs, so the walk hints stand beside them rather than at the
       ends of the zone. What fits is measured against the zone -- see
       fitPageTabs, which reads this element's parent. */
    flex: 0 1 auto;
}

/* ── A page is a box with its key on it ───────────────────────────────────── */

/*
 * The corners of the band are soft, not square.
 *
 * Every control in the header -- the tabs, the chip that counts what is
 * folded away, the action group and its buttons, the destinations -- was on
 * --radius-3, which at 40px tall reads as a corner that was meant to be
 * square and missed. They are all on --radius-6 now, and the keys printed on
 * them on --radius-4, so the family keeps its proportions. The token scales
 * with --theme-radius-scale, so a theme that asks for square corners (Retro
 * CRT Mk II) still gets them.
 */

.header-track .page-nav-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* A tab keeps its width. Allowed to shrink, the row squashed every tab
       past the one that fitted and the names printed over each other instead
       of the strip folding -- which is what fitPageTabs is for. */
    flex: 0 0 auto;
    /* Bigger than the draft's 28px: a numbered tab is one character wide, and
       at the draft's size the middle zone read as a row of loose digits rather
       than as the pages of the thing you are looking at. Raised twice since,
       because the pages are the one thing in this bar you aim at by name. */
    height: 40px;
    min-width: 2.6rem;
    padding: 0 1rem;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-6, 10px);
    /* The same surface the action group takes, so the two groups in the band
       are made of one material. On glass this is the see-through step; on the
       other depths it falls back to the block colour. */
    background: var(--surface-glass-page, var(--background-secondary));
    color: var(--text-tertiary);
    font-family: var(--font-family-main);
    font-size: 0.95rem;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    transition: background 0.2s, color 0.2s, border-color 0.2s, box-shadow 0.2s;
}

/* Written at the zone's own weight: the rule above is two classes and would
   otherwise re-display a tab that fitPageTabs has folded away. */
.header-track .page-nav-btn[hidden] {
    display: none;
}

.header-track .page-nav-btn:hover {
    color: var(--text-primary);
    border-color: color-mix(in srgb, var(--accent-primary) 55%, var(--border-primary));
}

.header-track .page-nav-btn.active {
    background: color-mix(in srgb, var(--accent-primary) 20%, transparent);
    border-color: var(--accent-primary);
    color: var(--text-primary);
    box-shadow: 0 0 12px color-mix(in srgb, var(--accent-primary) calc(40% * var(--theme-depth, 1)), transparent);
}

.header-track .page-tab-key {
    margin-left: 0.35rem;
    font-size: 0.68rem;
    font-weight: 400;
    opacity: 0.5;
    vertical-align: super;
}

/* ── What did not fit ─────────────────────────────────────────────────────── */

.header-track .page-nav-overflow {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    flex: 0 0 auto;
    height: 40px;
    padding: 0 0.75rem;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-6, 10px);
    background: var(--surface-glass-page, var(--background-secondary));
    color: var(--text-tertiary);
    font-family: var(--font-family-main);
    font-size: 0.95rem;
    font-weight: 600;
    line-height: 1;
    cursor: pointer;
}

.header-track .page-nav-overflow:hover {
    color: var(--text-primary);
    border-color: color-mix(in srgb, var(--accent-primary) 55%, var(--border-primary));
}

.page-nav-overflow-caret {
    flex: 0 0 auto;
    opacity: 0.7;
}

.page-nav-overflow-key {
    padding: 1px 5px;
    border-radius: var(--radius-4, 6px);
    background: color-mix(in srgb, var(--text-primary) 10%, transparent);
    color: var(--text-secondary);
    font-size: 0.68rem;
    font-weight: 700;
    opacity: 0.85;
}

/* ── Three weights for the same switcher ──────────────────────────────────────
   Everything above draws a page as its own 40px box. That is the loudest
   thing in a band whose other controls are 28px transparent icons, so it is
   one of three settings now (pageSwitcherStyle, Appearance -> Header and
   buttons). Written at the zone's own weight, because the rules above are
   two classes deep and would otherwise win.

   The tabs, the keys and the panel on `,` are the same in all three. This is
   how much room the switcher takes, not what it can do.
   ────────────────────────────────────────────────────────────────────────── */

/* Segmented (the default): one shell, the pages flat inside it.
   The shell is the box the tabs used to carry one each -- the same material,
   the same corner, the same 40px outer height as a destination -- and a
   segment inside it is 28px, which is what an action measures. */
body[data-page-switcher="segmented"] .header-track .page-navigation {
    height: 40px;
    align-items: center;
    gap: 2px;
    padding: 5px;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-6, 10px);
    background: var(--surface-glass-page, var(--background-secondary));
    box-shadow: var(--header-plate-shadow);
}

body[data-page-switcher="segmented"] .header-track .page-nav-btn,
body[data-page-switcher="segmented"] .header-track .page-nav-overflow {
    height: 28px;
    min-width: 2.1rem;
    padding: 0 0.75rem;
    /* No edge at all, not a transparent one: the shell is the box, and a
       segment that keeps a hairline of its own is the old drawing wearing a
       coat of paint. */
    border: none;
    border-radius: var(--radius-4, 6px);
    background: transparent;
    box-shadow: none;
    font-size: 0.85rem;
}

body[data-page-switcher="segmented"] .header-track .page-nav-btn:hover,
body[data-page-switcher="segmented"] .header-track .page-nav-overflow:hover {
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
    color: var(--text-primary);
}

/* The wash marks the segment; the shell already carries the edge and the
   cast, so the tab adds neither. */
body[data-page-switcher="segmented"] .header-track .page-nav-btn.active {
    background: color-mix(in srgb, var(--accent-primary) 22%, transparent);
    color: var(--text-primary);
    box-shadow: none;
}

/* Text: no box at all. The page you are on takes the 2px underline the
   destinations use, so "you are here" is said the same way in both halves of
   the band. */
body:is([data-page-switcher="text"], [data-page-switcher="classic"]) .header-track .page-navigation {
    gap: 1.1rem;
    overflow: visible;
}

/* Tabs, not a control panel: the walk arrows and the printed `,` read as
   stray marks beside words. Their keys keep working and are in the cheat
   sheet; the buttons only go from this style. */
body:is([data-page-switcher="text"], [data-page-switcher="classic"]) .header-track .page-walk-hint,
body:is([data-page-switcher="text"], [data-page-switcher="classic"]) .header-track .page-nav-overflow-key {
    display: none;
}

body:is([data-page-switcher="text"], [data-page-switcher="classic"]) .header-track .page-nav-btn,
body:is([data-page-switcher="text"], [data-page-switcher="classic"]) .header-track .page-nav-overflow {
    position: relative;
    height: 32px;
    min-width: 0;
    padding: 0 0.15rem;
    border: none;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
    font-size: 1rem;
    color: var(--text-secondary);
}

body:is([data-page-switcher="text"], [data-page-switcher="classic"]) .header-track .page-nav-btn:hover,
body:is([data-page-switcher="text"], [data-page-switcher="classic"]) .header-track .page-nav-overflow:hover {
    background: transparent;
    color: var(--text-primary);
}

body:is([data-page-switcher="text"], [data-page-switcher="classic"]) .header-track .page-nav-btn.active {
    background: transparent;
    color: var(--text-primary);
    font-weight: 700;
    box-shadow: none;
}

body:is([data-page-switcher="text"], [data-page-switcher="classic"]) .header-track .page-nav-btn.active::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: -0.35rem;
    height: 2px;
    border-radius: 2px;
    background: var(--accent-primary);
}

/* The chip is a word among words here, not a control: the key it carries is
   the same `,` printed on everything else that opens the panel. */
body:is([data-page-switcher="text"], [data-page-switcher="classic"]) .header-track .page-nav-overflow-key {
    background: transparent;
    padding: 0;
}

/* Classic: the text tabs, standing at the right beside the destinations
   rather than on the centre of the band -- where they stood before the band
   had a middle. The columns change, not the markup: the identity takes the
   free room, and the pages and the actions size to what they hold. */
body[data-page-switcher="classic"] .dashboard-section.section-controls .header-top {
    grid-template-columns: minmax(0, 1fr) minmax(0, auto) minmax(0, auto);
}

body[data-page-switcher="classic"] .dashboard-section.section-controls .header-track {
    justify-content: flex-end;
    justify-self: end;
}

/* On the destinations' line: as tall as they are, so the rule under the page
   you are on is the rule under the view you are in -- one line, not two. */
body[data-page-switcher="classic"] .header-track .page-nav-btn,
body[data-page-switcher="classic"] .header-track .page-nav-overflow {
    height: 40px;
}

body[data-page-switcher="classic"] .header-track .page-nav-btn.active::after {
    bottom: -2px;
}

/* Compact: one button naming the page you are on.
   pageTabCap() returns 1 for this style and the click opens the panel (see
   dashboard-page-nav.js), so what stands in the zone is a single control the
   size of an action, with a caret saying there is a list behind it. */
body[data-page-switcher="compact"] .header-track .page-nav-btn {
    height: 28px;
    min-width: 0;
    padding: 0 0.65rem;
    border-color: transparent;
    background: transparent;
    box-shadow: none;
    color: var(--text-primary);
    font-size: 0.85rem;
}

body[data-page-switcher="compact"] .header-track .page-nav-btn:hover {
    border-color: var(--border-primary);
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
}

body[data-page-switcher="compact"] .header-track .page-nav-btn.active {
    background: transparent;
    border-color: transparent;
    box-shadow: none;
}

/* Drawn rather than set in a glyph: two borders on a rotated square is a
   chevron at any size, and it takes the button's own ink. */
body[data-page-switcher="compact"] .header-track .page-nav-btn.active::after {
    content: '';
    width: 5px;
    height: 5px;
    margin: -3px 0 0 0.45rem;
    border-right: 1.6px solid currentColor;
    border-bottom: 1.6px solid currentColor;
    transform: rotate(45deg);
    opacity: 0.6;
}

/* The count would say the same thing the caret says. */
body[data-page-switcher="compact"] .header-track .page-nav-overflow {
    display: none;
}

/* ── Right: what you do, then where you go ────────────────────────────────── */

.dashboard-section.section-controls .header-actions {
    display: flex;
    align-items: center;
    gap: 1rem;
    flex: 0 0 auto;
    justify-content: flex-end;
}

/* Between what you do and where you go. */
.header-zone-divider {
    width: 1px;
    height: 24px;
    flex: 0 0 auto;
    background: var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
}

/* syncHeaderZoneDividers() hides it when either side of it is empty; `display`
   is set on the element here so the flex gap goes with the rule rather than
   leaving a hole where it stood. */
.header-zone-divider[hidden] {
    display: none;
}

/* The pages stand beside the actions only in classic; elsewhere the band's
   middle already separates them. */
body:not([data-page-switcher="classic"]) .header-zone-divider--pages {
    display: none;
}

/* The actions are one box of 28px squares, the way the draft draws them. */
.dashboard-section.section-controls .header-shortcuts {
    display: flex;
    align-items: center;
    /* Room for the key that sits on each button's corner: butted together, the
       marks landed on the neighbour's icon and neither could be read. The
       padding is what keeps those marks inside the group -- at 3px the last
       one hung past the page's own edge on a 1280 window. */
    gap: 5px;
    padding: 5px;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-6, 10px);
    background: var(--surface-glass-page, var(--background-secondary));
}

.header-actions .header-shortcuts .search-button {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    border: none;
    border-radius: var(--radius-6, 10px);
    background: transparent;
    box-shadow: none;
    backdrop-filter: none;
    /* The same ink as the destinations beside them. They carried a 0.6 on top
       of it, so two rows of the same colour read as two colours -- the actions
       a shade fainter than the pages, inbox, health and config buttons they
       sit next to. The keys keep the weight they had; see below. */
    color: var(--text-tertiary);
    overflow: visible;
    transition: opacity 0.2s, color 0.2s, background 0.2s;
}

.header-actions .header-shortcuts .search-button:hover,
.header-actions .header-shortcuts .search-button:focus-visible {
    color: var(--text-primary);
    background: color-mix(in srgb, var(--accent-primary) 20%, transparent);
    box-shadow: 0 0 12px color-mix(in srgb, var(--accent-primary) calc(40% * var(--theme-depth, 1)), transparent);
    transform: none;
}

.header-action-glyph {
    flex: 0 0 auto;
    pointer-events: none;
    /*
     * The same weight as a destination's icon, not just the same colour.
     *
     * They carry the same ink, but these were drawn at 16px with a 2px stroke
     * against the 18px and 2.2 of the pages, inbox, health and config buttons
     * beside them -- thinner lines covering fewer pixels read as a paler grey,
     * which is the difference the colour alone did not explain.
     */
    width: 18px;
    height: 18px;
    stroke-width: 2.2;
}

/*
 * The word is the accessible name, not a thing on screen.
 *
 * Five 28px squares is what the draft draws; a word beside each would be a
 * strip again. Each button keeps its aria-label and its tooltip, so the word
 * is a hover or a screen reader away.
 */
.header-actions .header-shortcuts .search-button-label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/*
 * The key sits on the corner of the button it belongs to.
 *
 * Opaque, so it reads over the glyph it labels: a translucent chip over an
 * icon is two drawings in the same 10 pixels and neither survives it. It wore
 * a hairline ring as well, which at this size is a box drawn around a single
 * character -- the ground it paints already separates it.
 */
.header-actions .header-shortcuts .search-button-icon {
    position: absolute;
    top: -4px;
    right: -4px;
    min-width: 12px;
    padding: 0 3px;
    border-radius: var(--radius-4, 6px);
    background: var(--background-primary);
    color: var(--text-secondary);
    font-family: var(--font-family-main);
    font-size: 0.58rem;
    font-weight: 700;
    line-height: 13px;
    text-align: center;
    /*
     * Undimmed. It carried the 0.6 the button used to wear, and
     * TestHeaderKeyChipContrastAcrossThemes measured what that cost across the
     * register: 114 of 224 themes put the key under 3:1, the worst at 2.16.
     * At full strength the worst is 4.33. The colour is unchanged --
     * --text-secondary, a step quieter than the glyph it labels -- so the chip
     * still reads as a note rather than as a second icon.
     */
    opacity: 1;
}

/* ── The destinations: the same square, standing on the band ──────────────── */

.header-destinations {
    display: flex;
    align-items: center;
    gap: 0.375rem;
}

/*
 * A destination is the size of the group of actions beside it.
 *
 * They were 28px squares standing next to a box that measures 40 -- the same
 * button, drawn smaller, because one of them had a surround and the other did
 * not. The outer size is what the eye compares, so that is what matches:
 * 28 + 2x5 of padding + 2x1 of border.
 */
.dashboard-section.section-controls .header-actions .dashboard-link-anchor,
.dashboard-section.section-controls .header-actions .docker-link-anchor,
.dashboard-section.section-controls .header-actions .library-link-anchor,
.dashboard-section.section-controls .header-actions .config-link-anchor,
.dashboard-section.section-controls .header-actions .page-nav-btn--inbox {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-6, 10px);
    background: var(--surface-glass-page, var(--background-secondary));
    color: var(--text-tertiary);
    overflow: visible;
    transition: color 0.2s, border-color 0.2s, background 0.2s;
}

.dashboard-section.section-controls .header-actions .dashboard-link-anchor:hover,
.dashboard-section.section-controls .header-actions .docker-link-anchor:hover,
.dashboard-section.section-controls .header-actions .library-link-anchor:hover,
.dashboard-section.section-controls .header-actions .config-link-anchor:hover,
.dashboard-section.section-controls .header-actions .page-nav-btn--inbox:hover {
    color: var(--text-primary);
    border-color: color-mix(in srgb, var(--accent-primary) 55%, var(--border-primary));
    background: color-mix(in srgb, var(--accent-primary) 12%, transparent);
}

.dashboard-section.section-controls .header-actions .config-link-icon {
    width: 18px;
    height: 18px;
}

/*
 * A count rides the corner of the square it counts for.
 *
 * Health drew its count as a pill standing beside the icon, which made the
 * health link twice the width of every other destination and the only one
 * whose width moved with its data. The draft puts both counts on the corner of
 * their own square: 14px, filled, white on the colour that says what it is.
 */
.dashboard-section.section-controls .header-actions .page-inbox-badge,
.dashboard-section.section-controls .header-actions .library-link--icon .health-badge,
.dashboard-section.section-controls .header-actions .docker-update-badge {
    position: absolute;
    top: -6px;
    right: -6px;
    left: auto;
    bottom: auto;
    display: flex;
    align-items: center;
    justify-content: center;
    /* 16px, not the draft's 14: two digits at 14px is a number you can see but
       not read. The ring in the band's colour is what lifts it off the icon. */
    min-width: 16px;
    width: auto;
    height: 16px;
    padding: 0 4px;
    border: none;
    border-radius: 8px;
    font-size: 0.66rem;
    font-weight: 700;
    line-height: 1;
    letter-spacing: 0;
    font-variant-numeric: tabular-nums;
    color: #fff;
    box-shadow:
        0 0 0 2px color-mix(in srgb, var(--background-secondary) 92%, transparent),
        0 1px 3px color-mix(in srgb, #000 40%, transparent);
    z-index: 2;
}

.dashboard-section.section-controls .header-actions .page-inbox-badge {
    background: var(--accent-primary);
    color: var(--background-primary);
}

/* Containers with an update waiting: news, not a fault -- the info colour. */
.dashboard-section.section-controls .header-actions .docker-update-badge {
    background: var(--accent-info, var(--accent-primary));
    color: var(--background-primary);
    pointer-events: none;
}

.dashboard-section.section-controls .header-actions .library-link--icon .health-badge {
    background: var(--accent-error);
}

.dashboard-section.section-controls .header-actions .library-link--icon .health-badge.health-badge-warn {
    background: var(--accent-warning);
    color: var(--text-on-warning, #000);
}

.dashboard-section.section-controls .header-actions .library-link--icon .health-badge.health-badge-down {
    background: var(--accent-error);
    color: #fff;
}

/* ── Depth: the band is a slab, the groups are plates on it ───────────────────
   The draft is a glass design: every surface carries a lit top edge, a dark
   bottom one, and a shadow whose depth says how far off the page it sits. All
   of it scales with --theme-depth, so "flat" in Appearance flattens the header
   the way it flattens everything else.
   ────────────────────────────────────────────────────────────────────────── */

/*
 * One plate, wherever it stands.
 *
 * The action group and the page tabs were built with two different casts --
 * the group's shadow read as a raised plate and the tabs' as a hairline, so
 * one half of the band looked pressed and the other drawn. This is the single
 * recipe both take, and the destinations with them.
 */
.dashboard-section.section-controls .header-top {
    --header-plate-shadow:
        inset 0 1px 0 color-mix(in srgb, var(--text-primary) calc(12% * var(--theme-depth, 1)), transparent),
        inset 0 -1px 0 color-mix(in srgb, #000 calc(22% * var(--theme-depth, 1)), transparent),
        0 1px 5px color-mix(in srgb, #000 calc(25% * var(--theme-depth, 1)), transparent);
}

.dashboard-section.section-controls .header-shortcuts {
    box-shadow: var(--header-plate-shadow);
}

/* A page tab is a plate too, one step shallower than the group beside it. */
/*
 * The tabs, the chip and the destinations take the group's plate.
 *
 * Each carries both edges: the top was lit and the bottom was not, so on a row
 * that lands on a half pixel the bottom hairline was the half that got rounded
 * away -- a box with no floor.
 */
.header-track .page-nav-btn,
.header-track .page-nav-overflow,
.dashboard-section.section-controls .header-actions .dashboard-link-anchor,
.dashboard-section.section-controls .header-actions .docker-link-anchor,
.dashboard-section.section-controls .header-actions .library-link-anchor,
.dashboard-section.section-controls .header-actions .config-link-anchor,
.dashboard-section.section-controls .header-actions .page-nav-btn--inbox {
    box-shadow: var(--header-plate-shadow);
}

/* The page you are on is lit from inside rather than raised. */
/* The page you are on keeps the plate and adds the bloom. */
.header-track .page-nav-btn.active {
    box-shadow:
        var(--header-plate-shadow),
        0 0 12px color-mix(in srgb, var(--accent-primary) calc(40% * var(--theme-depth, 1)), transparent);
}

/*
 * Pressing puts a control back into its plate.
 *
 * The lit edge moves from the top to the inside and the shadow under it goes,
 * which is the whole gesture -- raised, then pushed in.
 */
.header-actions .header-shortcuts .search-button:active,
.header-track .page-nav-btn:active,
.header-track .page-nav-overflow:active,
.dashboard-section.section-controls .header-actions .dashboard-link-anchor:active,
.dashboard-section.section-controls .header-actions .docker-link-anchor:active,
.dashboard-section.section-controls .header-actions .library-link-anchor:active,
.dashboard-section.section-controls .header-actions .config-link-anchor:active {
    box-shadow:
        inset 0 1px 3px color-mix(in srgb, #000 calc(45% * var(--theme-depth, 1)), transparent);
    transform: translateY(calc(0.5px * var(--theme-depth, 1)));
}

/* ── The band as the window narrows ───────────────────────────────────────────
   One line, and a fixed order of what it gives up when the zones no longer add
   up. The step is measured and written onto <body> by fitHeaderZones(); these
   rules are what each step means. Everything dropped here keeps its key, and
   the pages panel (`,`) still lists every page.
   ────────────────────────────────────────────────────────────────────────── */

/* Each step written past the zone's own rules on purpose: those are three
   classes deep (.dashboard-section.section-controls .header-shortcuts and
   friends), so a step keyed on one body attribute lost the cascade and hid
   nothing at all. */

/* 1 — the destinations. Shift+I, Shift+H and Shift+S still reach them. */
body[data-header-fit="1"] .dashboard-section.section-controls .header-destinations,
body[data-header-fit="2"] .dashboard-section.section-controls .header-destinations,
body[data-header-fit="3"] .dashboard-section.section-controls .header-destinations,
body[data-header-fit="4"] .dashboard-section.section-controls .header-destinations,
body[data-header-fit="1"] .dashboard-section.section-controls .header-zone-divider,
body[data-header-fit="2"] .dashboard-section.section-controls .header-zone-divider,
body[data-header-fit="3"] .dashboard-section.section-controls .header-zone-divider,
body[data-header-fit="4"] .dashboard-section.section-controls .header-zone-divider {
    display: none;
}

/* 2 — the actions, keys and all. */
body[data-header-fit="2"] .dashboard-section.section-controls .header-shortcuts,
body[data-header-fit="3"] .dashboard-section.section-controls .header-shortcuts,
body[data-header-fit="4"] .dashboard-section.section-controls .header-shortcuts {
    display: none;
}

/* 3 — the strip folds to the page you are on plus the chip (pageTabCap), and
   the keys that walk it go with the room they took.

   This was rung 4 while the pages button stood on its own between the track
   and the actions and had a rung of its own. It is one of the actions now, so
   rung 2 already takes it and there is nothing left for a rung of its own to
   give up. */
body[data-header-fit="3"] .dashboard-section.section-controls .page-walk-hint,
body[data-header-fit="4"] .dashboard-section.section-controls .page-walk-hint {
    display: none;
}

/* 4 — the clock and the weather, leaving the name and the switcher.
   Everything that stands for the clock goes with it: the line itself, the
   mini status beside it, and the compact date chip the narrow layout puts in
   its place -- on the smallest window all three were on screen at once. */
body[data-header-fit="4"] .dashboard-section.section-controls .header-top-primary,
body[data-header-fit="4"] .dashboard-section.section-controls #date-badge-mobile,
body[data-header-fit="4"] .dashboard-section.section-controls .date-badge-mobile:not(:empty) {
    display: none;
}

/*
 * The narrow layout is the last rung, whatever the measurement says.
 *
 * Below this width the row has the name and the page switcher and nothing
 * else: the compact chip, the clock line and the mini status all say the time,
 * and all three were drawn one under the other.
 */
@media (max-width: 767px) {
    /* Written at the clock placement's own weight (body[data-header-clock] …
       .header-identity .header-top-primary), which is what draws it. */
    body[data-header-clock] .dashboard-section.section-controls .header-identity .header-top-primary,
    body[data-header-clock] .dashboard-section.section-controls #date-badge-mobile,
    body[data-header-clock] .dashboard-section.section-controls .date-badge-mobile:not(:empty),
    body[data-header-clock] .dashboard-section.section-controls .header-actions,
    body[data-header-clock] .dashboard-section.section-controls .page-walk-hint {
        display: none;
    }

    /* Two zones left, so two tracks: the switcher sits straight after the
       name instead of holding the place of the columns that are gone. */
    body[data-header-clock] .dashboard-section.section-controls .header-top {
        /* The name gives way, not the switcher: at 360px an eight-letter page
           name plus a tab and a chip is 54px more than there is, and a name
           that is cut short still says where you are where a switcher pushed
           off the edge stops working. */
        grid-template-columns: minmax(0, 1fr) auto;
        padding: 0.5rem 0.75rem;
        gap: var(--space-2, 0.5rem);
    }

    body[data-header-clock] .dashboard-section.section-controls .title {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    body[data-header-clock] .dashboard-section.section-controls .header-track {
        justify-content: flex-start;
    }

    /* The name is the whole left zone here. Its block keeps a second column
       for the clock, and that column went on claiming 235px of a 520px window
       with the clock switched off -- so the switcher stood in the middle of
       the row rather than beside the name. */
    body[data-header-clock] .dashboard-section.section-controls .header-identity {
        display: block;
    }
}

/* Only the room around them: the template keeps its four tracks, and a zone
   that is display:none leaves the grid rather than pushing the rest onto a
   second row -- which is what a two-column override did here. */
body[data-header-fit="4"] .dashboard-section.section-controls .header-top,
body[data-header-fit="3"] .dashboard-section.section-controls .header-top {
    padding: 0.5rem 0.75rem;
    gap: var(--space-2, 0.5rem);
}

/* The keys printed on the action buttons are the first thing to go on a narrow
   window, before the buttons themselves: at 1180 the row still holds them. */
@media (max-width: 1180px) {
    .header-actions .header-shortcuts .search-button-icon {
        display: none;
    }
}

/*
 * The tag cloud is an action like the others.
 *
 * It was a corner FAB: fixed to the viewport, with a bottom-left anchor and a
 * pulse on its glyph. In the row it is a 28px square between search and
 * recents. The !important undoes dashboard-tag-cloud.css's own !important,
 * which hid it outside the grid -- it stands in every view now, and pressing
 * it from one goes back to the page it filters (see onToggleClick).
 */
.header-actions .header-shortcuts .tag-cloud-toggle-btn.search-button {
    /* relative, not static: the key on its corner is positioned against this
       button, and with no containing block of its own the chip fell back to
       whatever was positioned above it and landed at the page's right edge. */
    position: relative;
    z-index: auto;
    left: auto;
    bottom: auto;
    padding: 0;
    pointer-events: auto;
}

/*
 * In a view the cloud has nothing to filter, and the button keeps its place
 * rather than letting the row shift under the reader -- which is what the
 * `!important` is for. Not when the fold has put it away: then it has no place
 * on the row to keep.
 */
body[data-show-tag-cloud-button="true"] .header-shortcuts #tag-cloud-toggle-btn:not(.is-eligible):not(.is-folded) {
    display: inline-flex !important;
}

/*
 * The destination you are in is lit.
 *
 * Health, config and the inbox tab all carry `active` while their view is open
 * (setActivePageNavButton, syncHealthLinkActiveState, syncConfigLinkActiveState)
 * and nothing in the header read it -- so the bar looked the same whether you
 * were on the grid or three views deep. Same treatment the active page tab
 * takes, because it says the same thing.
 */
.dashboard-section.section-controls .header-actions .dashboard-link-anchor.active,
.dashboard-section.section-controls .header-actions .docker-link-anchor.active,
.dashboard-section.section-controls .header-actions .library-link-anchor.active,
.dashboard-section.section-controls .header-actions .config-link-anchor.active,
.dashboard-section.section-controls .header-actions .page-nav-btn--inbox.active {
    background: color-mix(in srgb, var(--accent-primary) 20%, transparent);
    border-color: var(--accent-primary);
    color: var(--text-primary);
    box-shadow:
        var(--header-plate-shadow),
        0 0 12px color-mix(in srgb, var(--accent-primary) calc(40% * var(--theme-depth, 1)), transparent);
}

/* ── The line under the band ─────────────────────────────────────────────────
   The second half of "waiting on a bookmark": a 2px rule under the header,
   drawn while an open is in flight. It is the convention every browser uses
   for the same wait, so it needs no explaining -- and unlike the row's own
   spinner it is visible from a scrolled page, from health, and from the inbox.
   ────────────────────────────────────────────────────────────────────────── */

.bookmark-open-progress {
    position: absolute;
    left: 0;
    right: 0;
    bottom: -1px;
    height: 2px;
    overflow: hidden;
    pointer-events: none;
    z-index: 3;
}

.dashboard-section.section-controls {
    position: relative;
}

.bookmark-open-progress-track {
    position: absolute;
    inset: 0 auto 0 0;
    width: 35%;
    background: linear-gradient(
        90deg,
        transparent,
        var(--accent-primary),
        transparent
    );
    animation: bookmark-open-progress-sweep 1.1s ease-in-out infinite;
}

@keyframes bookmark-open-progress-sweep {
    from { transform: translateX(-100%); }
    to { transform: translateX(340%); }
}

/* Standing still rather than sweeping: a band across the left third still says
   "in flight", and nothing on the page moves. */
body.no-animations .bookmark-open-progress-track {
    animation: none;
    background: color-mix(in srgb, var(--accent-primary) 70%, transparent);
}

@media (prefers-reduced-motion: reduce) {
    .bookmark-open-progress-track {
        animation: none;
        background: color-mix(in srgb, var(--accent-primary) 70%, transparent);
    }
}

/* ── Plain, or plated ─────────────────────────────────────────────────────────
   Two answers to the same question, and the reader picks (headerButtonStyle,
   Appearance -> Header and buttons).

   Plated is everything above: every control in a box of its own, on the glass
   surface, with an edge and a cast. Plain is the header as it was before the
   three-zone draft -- bare glyphs on the band, nothing around them, and a 2px
   accent rule under the one you are on.

   Written last in the file and at the same weight as the rules it undoes, so
   it wins by order rather than by piling on selectors. One answer for all
   three groups: a header with two of them plated and one bare reads as a
   mistake rather than as a choice.
   ────────────────────────────────────────────────────────────────────────── */

body[data-header-buttons="plain"] .header-track .page-navigation,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-shortcuts {
    border: none;
    background: none;
    box-shadow: none;
    padding: 0;
    height: auto;
}

/* Plated boxes the destinations and the page tabs one by one; the actions stay
   bare beside them. A surround around the actions on top of the boxes next to
   them drew a frame around a row that is already framed. */
body:not([data-header-buttons="plain"]) .dashboard-section.section-controls .header-shortcuts {
    border-color: transparent;
    background: none;
    box-shadow: none;
}

body[data-header-buttons="plain"] .header-track .page-nav-btn,
body[data-header-buttons="plain"] .header-track .page-nav-overflow,
body[data-header-buttons="plain"] .header-actions .header-shortcuts .search-button,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-actions .dashboard-link-anchor,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-actions .docker-link-anchor,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-actions .library-link-anchor,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-actions .config-link-anchor,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-actions .page-nav-btn--inbox {
    border: none;
    /* The rule that says where you are, kept transparent so nothing moves when
       it lights up. */
    border-bottom: 2px solid transparent;
    border-radius: 0;
    background: none;
    box-shadow: none;
    color: var(--text-tertiary);
}

body[data-header-buttons="plain"] .header-track .page-nav-btn:hover,
body[data-header-buttons="plain"] .header-track .page-nav-overflow:hover,
body[data-header-buttons="plain"] .header-actions .header-shortcuts .search-button:hover,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-actions .dashboard-link-anchor:hover,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-actions .docker-link-anchor:hover,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-actions .library-link-anchor:hover,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-actions .config-link-anchor:hover,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-actions .page-nav-btn--inbox:hover {
    border-color: transparent;
    background: none;
    box-shadow: none;
    color: var(--text-primary);
}

/* The page you are on, the view you are in: one rule, in the theme's accent. */
body[data-header-buttons="plain"] .header-track .page-nav-btn.active,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-actions .dashboard-link-anchor.active,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-actions .docker-link-anchor.active,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-actions .library-link-anchor.active,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-actions .config-link-anchor.active,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-actions .page-nav-btn--inbox.active {
    border-bottom-color: var(--accent-primary);
    background: none;
    box-shadow: none;
    color: var(--text-primary);
}

/* Pressed still says pressed, without a plate to sink into. */
body[data-header-buttons="plain"] .header-actions .header-shortcuts .search-button:active,
body[data-header-buttons="plain"] .header-track .page-nav-btn:active,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-actions .docker-link-anchor:active,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-actions .library-link-anchor:active,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-actions .config-link-anchor:active,
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-actions .dashboard-link-anchor:active {
    box-shadow: none;
    transform: none;
    opacity: 0.7;
}

/* Plated boxes the destinations; the actions and the page tabs stay bare in
   the header, and bare means bare on hover too -- the accent wash and its
   cast drew a box around a control the rest of the time drawn without one.
   The ink still lifts. A docked bar is a plate of its own and keeps its
   hover as it was. */
body:not([data-header-buttons="plain"]):not([data-action-bar="bottom"]):not([data-action-bar="left"]):not([data-action-bar="right"]) .dashboard-section.section-controls .header-shortcuts .search-button:is(:hover, :focus-visible, :active),
body:not([data-header-buttons="plain"]):not([data-page-switcher="segmented"]) .dashboard-section.section-controls .header-track :is(.page-nav-btn, .page-nav-overflow):is(:hover, :active) {
    background: transparent;
    box-shadow: none;
    color: var(--text-primary);
}

/* The zones sit closer together with no boxes between them, and the rule that
   separated the two groups has nothing to separate. */
body[data-header-buttons="plain"] .dashboard-section.section-controls .header-shortcuts {
    gap: var(--space-2, 0.5rem);
}

body[data-header-buttons="plain"] .header-zone-divider {
    display: none;
}

/* ── The compact switcher's menu ──────────────────────────────────────────────
   Compact used to be one tab that opened the full pages panel: a modal over
   the page, with its own scroll and its own way out, to answer "which page am
   I going to". The list is short and the question is small, so it is answered
   where it is asked -- under the control, on the same surface the context
   menus use.
   ────────────────────────────────────────────────────────────────────────── */

.page-switcher-menu {
    min-width: 13rem;
    max-width: min(18rem, calc(100vw - 1rem));
    padding: var(--space-1-5, 0.35rem);
    display: flex;
    flex-direction: column;
    gap: 2px;
    /* The list scrolls, not the menu: the filter above it and the two rows
       under it stay in place while the pages move. */
    overflow: hidden;
}

.page-switcher-filter {
    padding: 0 0 var(--space-1, 0.25rem);
}

.page-switcher-filter-input {
    width: 100%;
    box-sizing: border-box;
    padding: 0.3rem 0.5rem;
    border: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
    border-radius: var(--radius-4, 6px);
    background: color-mix(in srgb, var(--text-primary) 4%, transparent);
    color: var(--text-primary);
    font-family: var(--font-family-main);
    font-size: calc(var(--font-size-controls) * 0.9);
}

.page-switcher-filter-input:focus-visible {
    outline: none;
    border-color: var(--accent-primary);
}

.page-switcher-list {
    display: flex;
    flex-direction: column;
    gap: 1px;
    max-height: min(50vh, 15rem);
    overflow-y: auto;
    min-height: 0;
}

.page-switcher-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3, 0.75rem);
    width: 100%;
    padding: 0.35rem 0.55rem;
    border: 0;
    border-radius: var(--radius-4, 6px);
    background: transparent;
    color: var(--text-secondary);
    font-family: var(--font-family-main);
    font-size: calc(var(--font-size-controls) * 0.95);
    text-align: left;
    cursor: pointer;
}

.page-switcher-item:hover,
.page-switcher-item:focus-visible {
    outline: none;
    background: color-mix(in srgb, var(--accent-primary) 12%, transparent);
    color: var(--text-primary);
}

/* The page you are on, marked the way the strip marks it. */
.page-switcher-item.is-current {
    color: var(--text-primary);
    box-shadow: inset 2px 0 0 var(--accent-primary);
}

.page-switcher-item-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.page-switcher-item-key {
    flex: 0 0 auto;
    color: var(--text-tertiary);
    font-size: calc(var(--font-size-controls) * 0.8);
    font-variant-numeric: tabular-nums;
}

/* Under a hairline: these two are about the list rather than in it. */
.page-switcher-foot {
    display: flex;
    flex-direction: column;
    gap: 1px;
    margin-top: var(--space-1, 0.25rem);
    padding-top: var(--space-1, 0.25rem);
    border-top: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
}

.page-switcher-create {
    padding: var(--space-1, 0.25rem) 0;
}

/* ── The actions that did not fit ─────────────────────────────────────────────
   The bar can hold nine buttons and most readers use three or four. Past the
   reader's own cap the rest stand behind one control that says how many it
   holds -- the same bargain the page tabs make with their "+N" chip. The
   folded buttons stay in the DOM: the menu opens them by pressing them, so
   what each action does is written once.
   ────────────────────────────────────────────────────────────────────────── */

.header-shortcuts .search-button.is-folded {
    display: none;
}

.header-action-overflow .header-action-overflow-count {
    font-size: calc(var(--font-size-controls) * 0.8);
    font-variant-numeric: tabular-nums;
    margin-left: 0.15rem;
}

.header-action-menu {
    min-width: 12rem;
    padding: var(--space-1-5, 0.35rem);
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.header-action-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3, 0.75rem);
    width: 100%;
    padding: 0.35rem 0.55rem;
    border: 0;
    border-radius: var(--radius-4, 6px);
    background: transparent;
    color: var(--text-secondary);
    font-family: var(--font-family-main);
    font-size: calc(var(--font-size-controls) * 0.95);
    text-align: left;
    cursor: pointer;
}

.header-action-item:hover,
.header-action-item:focus-visible {
    outline: none;
    background: color-mix(in srgb, var(--accent-primary) 12%, transparent);
    color: var(--text-primary);
}

.header-action-item-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The key that does the same thing without the menu. */
.header-action-item-key {
    flex: 0 0 auto;
    color: var(--text-tertiary);
    font-size: calc(var(--font-size-controls) * 0.8);
    font-weight: var(--font-weight-bold);
}

/* Last on the row, whatever the buttons in front of it are ordered to. */
.header-actions .header-shortcuts .header-action-overflow { order: 99; }

/* ── Recents as a sheet under the header ──────────────────────────────────────
   Same bargain the pages panel makes: it is opened from the band and answers a
   question about the page under it, so it hangs from the band rather than
   floating over a dimmed screen. The chrome -- the anchor, the square top
   corners, the drop -- is .header-sheet in modal.css; what is here is the list.

   And the list is drawn the way every list in the product is drawn: rows of
   52px with the key the reader would type at the head of each one. It was a
   27rem column of 27px lines, which is what a narrow panel wants and what a
   band across the page cannot use.
   ────────────────────────────────────────────────────────────────────────── */

.modal.recents-sheet .recent-bookmarks-modal-list {
    gap: 0;
    padding: 0;
    max-height: min(52vh, 24rem);
}

.modal.recents-sheet .recent-bookmarks-modal-item {
    min-height: 52px;
    padding: 0 var(--space-3, 0.8rem);
    border-radius: 0;
    border-bottom: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
    gap: var(--space-3, 0.75rem);
    /* key, favicon, name, category, how long ago, opens */
    grid-template-columns: 4.5ch 1.1rem minmax(0, 1fr) auto auto auto;
}

.modal.recents-sheet .recent-bookmarks-modal-name {
    font-size: var(--font-size-controls);
}

/*
 * The key column stands whether or not the bookmark has one, so the names line
 * up down the column instead of each starting where the one above it ended.
 */
.recent-bookmarks-modal-shortcut {
    min-width: 4.5ch;
    color: var(--accent-primary);
    font-size: calc(var(--font-size-controls) * 0.78);
    font-weight: var(--font-weight-bold);
    letter-spacing: 0.04em;
    font-variant-numeric: tabular-nums;
}

.modal.recents-sheet .recent-bookmarks-modal-recency,
.modal.recents-sheet .recent-bookmarks-modal-opens {
    font-variant-numeric: tabular-nums;
}

/* Wide: the rows lay out across the page's column, three at most. */
.modal.recents-sheet.is-grid .recent-bookmarks-modal-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(30%, 1fr));
    overflow-y: auto;
    min-height: 0;
}

.modal.recents-sheet.is-grid .recent-bookmarks-modal-item {
    border-right: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
}

@media (max-width: 1100px) {
    .modal.recents-sheet.is-grid .recent-bookmarks-modal-list {
        grid-template-columns: repeat(auto-fill, minmax(45%, 1fr));
    }
}

@media (max-width: 720px) {
    .modal.recents-sheet.is-grid .recent-bookmarks-modal-list {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* Narrow: as wide as what it holds, rows side by side while they fit. */
.modal.recents-sheet.is-compact .recent-bookmarks-modal-list {
    display: flex;
    flex-wrap: wrap;
}

.modal.recents-sheet.is-compact .recent-bookmarks-modal-item {
    flex: 1 1 18rem;
    border-right: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
}

/*
 * The actions and the foot run the sheet's width, on one line each.
 *
 * In a 27rem panel the buttons stacked over their hint; across the page they
 * are a row with the command that does the same thing at the far end, the way
 * the pages sheet puts its count and its keys at either end of the foot.
 */
.modal.recents-sheet .recent-bookmarks-modal-toolbar {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4, 1rem);
    margin-top: var(--space-2, 0.55rem);
}

.modal.recents-sheet .recent-bookmarks-open-hint {
    margin: 0;
    text-align: right;
}

/*
 * One bookmark is not a list, and none is not an empty list: with a single
 * entry the row is the action, so the buttons that open "the last five" have
 * nothing to act on; with none the panel says what fills it rather than
 * drawing an empty box with a key hint under it.
 */
.modal.recents-sheet.is-single .recent-bookmarks-modal-toolbar,
.modal.recents-sheet.is-empty .recent-bookmarks-modal-toolbar,
.modal.recents-sheet.is-empty .recent-bookmarks-modal-foot {
    display: none;
}

/* ── Where the action buttons stand ───────────────────────────────────────────
   The same group, moved. In the header it is part of the band; out of it, it
   is a dock of its own: at the bottom where the old button bar stood, or a
   column on either side at half height. A dock always draws its own plate,
   whatever the header's button style says -- a bare row floating over the
   bookmarks has nothing telling it apart from them.
   ────────────────────────────────────────────────────────────────────────── */

body[data-action-bar="bottom"] .dashboard-section.section-controls .header-shortcuts,
body[data-action-bar="left"] .dashboard-section.section-controls .header-shortcuts,
body[data-action-bar="right"] .dashboard-section.section-controls .header-shortcuts {
    position: fixed;
    z-index: 900;
    height: auto;
    padding: 6px;
    gap: 6px;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-6, 10px);
    background: var(--surface-3, var(--background-secondary));
    box-shadow: 0 8px 24px color-mix(in srgb, #000 35%, transparent);
}

body[data-action-bar="bottom"] .dashboard-section.section-controls .header-shortcuts {
    left: 50%;
    bottom: 16px;
    transform: translateX(-50%);
    flex-direction: row;
}

body[data-action-bar="left"] .dashboard-section.section-controls .header-shortcuts,
body[data-action-bar="right"] .dashboard-section.section-controls .header-shortcuts {
    top: 50%;
    transform: translateY(-50%);
    flex-direction: column;
}

body[data-action-bar="left"] .dashboard-section.section-controls .header-shortcuts {
    left: 12px;
}

body[data-action-bar="right"] .dashboard-section.section-controls .header-shortcuts {
    right: 12px;
}

/* The key chip on each action button can go; the keys keep working. */
body[data-action-keys="off"] .header-shortcuts .search-button-icon,
body[data-action-keys="off"] .header-action-item-key {
    display: none;
}

/*
 * The key, on hover, when the chips are off (action-key-hint.js).
 *
 * Above the docked bar (900) and never in the way of the pointer: it stands
 * beside the button it names, and a click passes through it.
 */
.action-key-hint {
    position: fixed;
    z-index: 1002;
    padding: var(--space-1, 0.25rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-4, 7px);
    background: var(--background-secondary);
    box-shadow: 0 4px 14px color-mix(in srgb, black 22%, transparent);
    pointer-events: none;
}

.action-key-hint kbd {
    display: block;
    min-width: 1.4em;
    padding: var(--space-hairline, 0.05rem) var(--space-1-5, 0.32rem);
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-3, 0.25rem);
    background: var(--background-primary);
    color: var(--text-primary);
    font-family: var(--font-family-main);
    font-size: var(--font-size-controls);
    line-height: 1.3;
    text-align: center;
}

/* Switched off: no actions anywhere, and no divider waiting for them. */
body[data-action-bar-enabled="off"] .dashboard-section.section-controls .header-shortcuts,
body[data-action-bar-enabled="off"] .dashboard-section.section-controls .header-zone-divider {
    display: none;
}

/* A dock slides into the edge it stands on, and back out. The resting
   transforms above stay as they are; hidden adds the way out. */
body[data-action-bar="bottom"] .dashboard-section.section-controls .header-shortcuts,
body[data-action-bar="left"] .dashboard-section.section-controls .header-shortcuts,
body[data-action-bar="right"] .dashboard-section.section-controls .header-shortcuts {
    transition: transform 0.2s ease, opacity 0.2s ease;
}

body[data-action-bar-hidden][data-action-bar="bottom"] .dashboard-section.section-controls .header-shortcuts {
    transform: translate(-50%, calc(100% + 24px));
    opacity: 0;
    pointer-events: none;
}

body[data-action-bar-hidden][data-action-bar="left"] .dashboard-section.section-controls .header-shortcuts {
    transform: translate(calc(-100% - 20px), -50%);
    opacity: 0;
    pointer-events: none;
}

body[data-action-bar-hidden][data-action-bar="right"] .dashboard-section.section-controls .header-shortcuts {
    transform: translate(calc(100% + 20px), -50%);
    opacity: 0;
    pointer-events: none;
}

/*
 * The intro (action-bar-autohide.js, setting actionBarIntroStyle): how the
 * buttons come in as the bar comes into view. The host carries the style and
 * is-intro-playing, each button .action-bar-intro and --intro-index, its turn
 * in steps. The drawings on the setting's cards (setting-art.js) use the same
 * classes and play while their card has the pointer or the focus. `scale`,
 * not transform, so the dock's own transforms and the buttons' hover stay
 * theirs.
 *
 * swell, swell-middle, swell-together: each button grows to half again its
 * size and settles back slowly -- left to right, out from the middle, or all
 * at once. unfold: the bar opens along its length while the buttons pop in
 * left to right. accordion: the same, from a narrower start, the buttons
 * popping out from the middle.
 */
:is(.is-intro-playing, .config-choice-card:is(:hover, :focus-visible) .setting-art-intro)[data-intro-style^="swell"] .action-bar-intro {
    animation: action-bar-intro-swell 1100ms both;
    animation-delay: calc(var(--intro-index, 0) * 70ms);
}

:is(.is-intro-playing, .config-choice-card:is(:hover, :focus-visible) .setting-art-intro):is([data-intro-style="unfold"], [data-intro-style="accordion"]) {
    --intro-unfold-from: 0.25 1;
    animation: action-bar-intro-unfold 380ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
}

:is(.is-intro-playing, .config-choice-card:is(:hover, :focus-visible) .setting-art-intro)[data-intro-style="accordion"] {
    --intro-unfold-from: 0.15 1;
}

:is(.is-intro-playing, .config-choice-card:is(:hover, :focus-visible) .setting-art-intro)[data-intro-style="unfold"][data-intro-axis="v"] {
    --intro-unfold-from: 1 0.25;
}

:is(.is-intro-playing, .config-choice-card:is(:hover, :focus-visible) .setting-art-intro)[data-intro-style="accordion"][data-intro-axis="v"] {
    --intro-unfold-from: 1 0.15;
}

:is(.is-intro-playing, .config-choice-card:is(:hover, :focus-visible) .setting-art-intro)[data-intro-style="unfold"] .action-bar-intro {
    animation: action-bar-intro-pop 340ms cubic-bezier(0.3, 1.6, 0.5, 1) both;
    animation-delay: calc(var(--intro-index, 0) * 45ms);
}

:is(.is-intro-playing, .config-choice-card:is(:hover, :focus-visible) .setting-art-intro)[data-intro-style="accordion"] .action-bar-intro {
    animation: action-bar-intro-pop 360ms cubic-bezier(0.3, 1.6, 0.5, 1) both;
    animation-delay: calc(var(--intro-index, 0) * 60ms);
}

@keyframes action-bar-intro-swell {
    0% {
        scale: 0.4;
        opacity: 0;
        animation-timing-function: cubic-bezier(0.2, 0.9, 0.3, 1);
    }
    22% {
        scale: 1.5;
        opacity: 1;
        animation-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
    }
    100% {
        scale: 1;
    }
}

@keyframes action-bar-intro-unfold {
    from {
        scale: var(--intro-unfold-from);
        opacity: 0.3;
    }
}

@keyframes action-bar-intro-pop {
    from {
        scale: 0;
        opacity: 0;
    }
}

/*
 * The handle on the edge a bar slid into (action-bar-autohide.js).
 *
 * Plain enough at rest that the reader keeps knowing where the bar went, and
 * larger and clearer as the pointer nears, through --edge-proximity (0 to 1). Only drawn while a
 * docked bar is away; a click on it brings the bar back. No backdrop-filter:
 * it takes clicks.
 */
.action-bar-edge-handle {
    --edge-proximity: 0;
    --edge-thick: calc(10px + 8px * var(--edge-proximity));
    --edge-long: calc(64px + 40px * var(--edge-proximity));
    position: fixed;
    z-index: 899;
    display: none;
    background: color-mix(in srgb, var(--background-secondary) 92%, var(--text-primary));
    border: 1px solid color-mix(in srgb, var(--text-primary) 16%, transparent);
    opacity: calc(0.65 + 0.3 * var(--edge-proximity));
    cursor: pointer;
    animation: action-bar-edge-handle-in 0.2s ease both;
}

.action-bar-edge-handle::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    translate: -50% -50%;
    border-radius: 2px;
    background: color-mix(in srgb, var(--text-primary) calc(50% + 30% * var(--edge-proximity)), transparent);
}

@keyframes action-bar-edge-handle-in {
    from { opacity: 0; }
}

body[data-action-bar-hidden]:is([data-action-bar="left"], [data-action-bar="right"], [data-action-bar="bottom"]) .action-bar-edge-handle {
    display: block;
}

body[data-action-bar="left"] .action-bar-edge-handle,
body[data-action-bar="right"] .action-bar-edge-handle {
    top: 50%;
    translate: 0 -50%;
    width: var(--edge-thick);
    height: var(--edge-long);
}

body[data-action-bar="left"] .action-bar-edge-handle {
    left: 0;
    border-left: 0;
    border-radius: 0 7px 7px 0;
}

body[data-action-bar="right"] .action-bar-edge-handle {
    right: 0;
    border-right: 0;
    border-radius: 7px 0 0 7px;
}

body[data-action-bar="bottom"] .action-bar-edge-handle {
    bottom: 0;
    left: 50%;
    translate: -50% 0;
    width: var(--edge-long);
    height: var(--edge-thick);
    border-bottom: 0;
    border-radius: 7px 7px 0 0;
}

body:is([data-action-bar="left"], [data-action-bar="right"]) .action-bar-edge-handle::after {
    width: 2px;
    height: calc(22px + 14px * var(--edge-proximity));
}

body[data-action-bar="bottom"] .action-bar-edge-handle::after {
    width: calc(22px + 14px * var(--edge-proximity));
    height: 2px;
}

/* Nothing left of the actions in the header, so nothing to divide. */
body[data-action-bar="bottom"] .dashboard-section.section-controls .header-zone-divider,
body[data-action-bar="left"] .dashboard-section.section-controls .header-zone-divider,
body[data-action-bar="right"] .dashboard-section.section-controls .header-zone-divider {
    display: none;
}

/* Room so the dock never sits on the last row or the first column. */
body[data-action-bar="bottom"] #dashboard-layout {
    padding-bottom: 72px;
}

/* A side column stands in the page's own margin, so nothing moves to make
   room for it. Only where that margin is too narrow to hold it is room made,
   on both sides so the grid keeps the centre the header stands on. */
@media (max-width: 1199px) {
    body[data-action-bar="left"] #dashboard-layout,
    body[data-action-bar="right"] #dashboard-layout {
        padding-left: 56px;
        padding-right: 56px;
    }
}

/* A side column has no room on a phone; it goes to the bottom instead. */
@media (max-width: 700px) {
    body[data-action-bar="left"] .dashboard-section.section-controls .header-shortcuts,
    body[data-action-bar="right"] .dashboard-section.section-controls .header-shortcuts {
        top: auto;
        bottom: 16px;
        left: 50%;
        right: auto;
        transform: translateX(-50%);
        flex-direction: row;
    }

    /* On a phone the side columns stand at the bottom, so they leave downwards. */
    body[data-action-bar-hidden][data-action-bar="left"] .dashboard-section.section-controls .header-shortcuts,
    body[data-action-bar-hidden][data-action-bar="right"] .dashboard-section.section-controls .header-shortcuts {
        transform: translate(-50%, calc(100% + 24px));
    }

    body[data-action-bar="left"] #dashboard-layout,
    body[data-action-bar="right"] #dashboard-layout {
        padding-left: 0;
        padding-right: 0;
        padding-bottom: 72px;
    }
}

/*
 * Suggested tags, on the row that has none.
 *
 * Two halves of one chip: the tag, which takes it, and a × that turns it down
 * for good. Drawn quietly -- an offer is not a fact about the link, and a row
 * of loud chips over an untagged list reads as tags that are already there.
 * Shared by the inbox queue and the Bookmarks view.
 */
.tag-suggest-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1, 0.25rem);
    align-items: center;
}

.inbox-item .tag-suggest-chips {
    margin-top: var(--space-1, 0.25rem);
}

.tag-suggest-chip {
    display: inline-flex;
    align-items: center;
    border: 1px dashed var(--border-primary);
    border-radius: var(--radius-pill, 999px);
    background: transparent;
    line-height: 1;
}

.tag-suggest-chip-add,
.tag-suggest-chip-dismiss {
    border: 0;
    background: transparent;
    color: var(--text-secondary);
    font-size: var(--font-size-small, 0.75rem);
    font-family: inherit;
    cursor: pointer;
    padding: 0.15rem 0.4rem;
}

.tag-suggest-chip-add:hover,
.tag-suggest-chip-add:focus-visible {
    color: var(--accent-primary);
}

.tag-suggest-chip-dismiss {
    padding-left: 0.1rem;
    padding-right: 0.35rem;
    opacity: 0.6;
}

.tag-suggest-chip-dismiss:hover,
.tag-suggest-chip-dismiss:focus-visible {
    opacity: 1;
    color: var(--danger, #d24545);
}

/* The preview of what a selection would be tagged, in the Move to… shape. */
.unsorted-suggest-list {
    display: flex;
    flex-direction: column;
    max-height: 16rem;
    overflow-y: auto;
}

.unsorted-suggest-item {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    cursor: pointer;
}

.unsorted-suggest-rows {
    margin-left: auto;
    color: var(--text-secondary);
    font-size: var(--font-size-small, 0.75rem);
}

.unsorted-suggest-apply {
    margin: var(--space-2, 0.5rem);
    padding: 0.3rem 0.6rem;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-2, 0.25rem);
    background: var(--background-secondary);
    color: var(--text-primary);
    font-family: inherit;
    font-size: var(--font-size-controls);
    cursor: pointer;
}

.unsorted-suggest-apply:hover,
.unsorted-suggest-apply:focus-visible {
    border-color: var(--accent-primary);
    color: var(--accent-primary);
}

/*
 * Keep, as it happens: a copy of the row flying to the Bookmarks icon, where
 * Unsorted is. Fixed and above every overlay, because the
 * flight starts on the triage card as often as on a row.
 */
.keep-flight {
    position: fixed;
    z-index: 13000;
    pointer-events: none;
    display: flex;
    align-items: center;
    padding: 0 var(--space-3, 0.75rem);
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    border: 1px solid var(--accent-primary);
    border-radius: var(--radius-5, 8px);
    background: var(--background-secondary);
    color: var(--text-primary);
    font-family: var(--font-family-main);
    font-weight: var(--font-weight-semibold);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
    transform-origin: center;
}

@keyframes keep-count-bump {
    0% { transform: scale(1); }
    35% { transform: scale(1.6); color: var(--accent-primary); }
    100% { transform: scale(1); }
}

.inbox-triage-kept-count.is-bumped {
    display: inline-block;
    animation: keep-count-bump 0.7s ease-out;
}

/* The triage card's own counter: the tab it would fly to sits under the
   overlay, so the card carries a small one of its own. */
.inbox-triage-kept-count {
    margin-left: auto;
    margin-right: var(--space-2, 0.5rem);
    padding: 0.1rem 0.5rem;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-pill, 999px);
    font-size: var(--font-size-small, 0.75rem);
    color: var(--text-secondary);
}


/* ==== css/dashboard-bookmark-row.css ==== */
/* Bookmark row hover / selection — loaded after dashboard.css so layout presets cannot override */

:root {
    --bookmark-row-hover-bg: linear-gradient(
        90deg,
        color-mix(in srgb, var(--accent-primary) 22%, var(--background-secondary)) 0%,
        color-mix(in srgb, var(--accent-primary) 14%, var(--background-primary)) 48%,
        color-mix(in srgb, var(--accent-primary) 6%, var(--background-primary)) 100%
    );
    --bookmark-row-selected-bg: linear-gradient(
        90deg,
        color-mix(in srgb, var(--accent-primary) 30%, var(--background-secondary)) 0%,
        color-mix(in srgb, var(--accent-primary) 18%, var(--background-primary)) 48%,
        color-mix(in srgb, var(--accent-primary) 8%, var(--background-primary)) 100%
    );
    --bookmark-row-dimmed-bg: linear-gradient(
        90deg,
        color-mix(in srgb, var(--accent-primary) 12%, var(--background-secondary)) 0%,
        color-mix(in srgb, var(--accent-primary) 7%, var(--background-primary)) 48%,
        color-mix(in srgb, var(--accent-primary) 3%, var(--background-primary)) 100%
    );

    /* The 100% stop of each gradient above, on its own.
     *
     * In "hover" mode the shortcut label floats in over the right end of the
     * row, which is where a long name has been truncated to -- so the label and
     * the fade under it have to be painted in the colour the row actually is at
     * that edge. A flat --background-primary would be close but visibly flat
     * against a tinted row, and these three keep the two in step by definition:
     * change a gradient's last stop and you change this with it.
     *
     * Not derived from the gradients themselves because CSS cannot sample one.
     * layout-modern-tokens.css overrides all three with the modern percentages,
     * the same way it already aliases the gradients. */
    --bookmark-row-edge-hover: color-mix(in srgb, var(--accent-primary) 6%, var(--background-primary));
    --bookmark-row-edge-selected: color-mix(in srgb, var(--accent-primary) 8%, var(--background-primary));
    --bookmark-row-edge-dimmed: color-mix(in srgb, var(--accent-primary) 3%, var(--background-primary));

    /* Whichever of the three the row is wearing right now. Plain page
       background until a state rule says otherwise. */
    --bookmark-row-edge: var(--background-primary);
}
/*
 * A row that lights up further, for anyone who wants to see it from across
 * the desk.
 *
 * The same gradient as above, lit from the same edge; only how far the accent
 * carries differs. It arrived with the modern layout, where it was one of
 * twenty-one tokens describing a whole visual language -- and it is the one
 * of them that is a preference about rows rather than a layout. Lifted here
 * so it survives that layout being folded away, and so somebody on classic
 * can have it without taking the rest.
 *
 * The edge colours follow the 100% stop of each gradient, the way the subtle
 * set does: the shortcut label floating over a row's right edge has to sit on
 * the colour that is actually under it.
 */
body[data-row-highlight="strong"] {
    --bookmark-row-hover-bg: linear-gradient(
        90deg,
        color-mix(in srgb, var(--accent-primary) 58%, var(--background-secondary)) 0%,
        color-mix(in srgb, var(--accent-primary) 32%, var(--background-primary)) 48%,
        color-mix(in srgb, var(--accent-primary) 10%, var(--background-primary)) 100%
    );
    --bookmark-row-selected-bg: linear-gradient(
        90deg,
        color-mix(in srgb, var(--accent-primary) 68%, var(--background-secondary)) 0%,
        color-mix(in srgb, var(--accent-primary) 38%, var(--background-primary)) 48%,
        color-mix(in srgb, var(--accent-primary) 16%, var(--background-primary)) 100%
    );
    --bookmark-row-dimmed-bg: linear-gradient(
        90deg,
        color-mix(in srgb, var(--accent-primary) 12%, var(--background-secondary)) 0%,
        color-mix(in srgb, var(--accent-primary) 7%, var(--background-primary)) 48%,
        color-mix(in srgb, var(--accent-primary) 3%, var(--background-primary)) 100%
    );
    --bookmark-row-edge-hover: color-mix(in srgb, var(--accent-primary) 10%, var(--background-primary));
    --bookmark-row-edge-selected: color-mix(in srgb, var(--accent-primary) 16%, var(--background-primary));
    --bookmark-row-edge-dimmed: color-mix(in srgb, var(--accent-primary) 3%, var(--background-primary));
}


.bookmark-link:not(.bookmark-inline-editing) {
    transition:
        background 0.16s ease,
        box-shadow 0.16s ease,
        border-color 0.16s ease,
        color 0.16s ease;
}

body.no-animations .bookmark-link:not(.bookmark-inline-editing) {
    transition: none;
}

/* Status rows: keep extra inset so the left status bar does not crowd favicons. */
.bookmarks-list[data-bookmarks-list="true"]:not([data-smart-collection="true"]) .bookmark-link:is(.status-online, .status-offline, .status-checking):not(.bookmark-inline-editing) {
    padding-left: calc(var(--bookmark-row-start-pad, 6px) + 4px);
    border-radius: var(--layout-radius-sm, 8px);
}

.bookmark-link:is(:hover, .keyboard-selected, :focus-within):not(.bookmark-inline-editing) {
    color: var(--text-primary);
    background: var(--bookmark-row-hover-bg);
    --bookmark-row-edge: var(--bookmark-row-edge-hover);
    border-radius: var(--layout-radius-sm, 8px);
}

.bookmark-link:is(.keyboard-selected, :focus-within):not(.bookmark-inline-editing) {
    background: var(--bookmark-row-selected-bg);
    --bookmark-row-edge: var(--bookmark-row-edge-selected);
}

.bookmark-link:is(:hover, .keyboard-selected, :focus-within):not(.bookmark-inline-editing) .bookmark-superscript-badge,
.bookmark-link:is(:hover, .keyboard-selected, :focus-within):not(.bookmark-inline-editing) .status-text,
.bookmark-link:is(:hover, .keyboard-selected, :focus-within):not(.bookmark-inline-editing) .bookmark-shortcut {
    background: transparent;
    border-color: color-mix(in srgb, var(--accent-primary) 35%, var(--border-secondary));
    color: var(--text-secondary);
}

a.bookmark-open:focus-visible {
    outline: none;
}

/* Status rows: tint idle rows only; hover/selection use default bookmark chrome */
body:not(.status-color-lock) .bookmarks-list[data-bookmarks-list="true"]:not([data-smart-collection="true"]) .bookmark-link.status-offline:not(:hover):not(.keyboard-selected):not(:focus-within):not(.bookmark-inline-editing) {
    color: var(--status-offline-fg, var(--accent-error, #ef4444));
    background: var(--status-offline-bg, color-mix(in srgb, var(--accent-error, #ef4444) 24%, var(--background-primary)));
    box-shadow: inset 3px 0 0 var(--status-offline-bar, var(--accent-error, #ef4444));
}

body:not(.status-color-lock) .bookmarks-list[data-bookmarks-list="true"]:not([data-smart-collection="true"]) .bookmark-link.status-checking:not(:hover):not(.keyboard-selected):not(:focus-within):not(.bookmark-inline-editing) {
    color: var(--status-checking-fg, var(--accent-warning, #f59e0b));
    background: var(--status-checking-bg, color-mix(in srgb, var(--accent-warning, #f59e0b) 16%, var(--background-primary)));
}

/* While the mouse explores bookmarks, soften the stale keyboard selection until the next keyboard move. */
body.bookmark-kbd-selection-dimmed .bookmark-link.keyboard-selected:not(:hover):not(.bookmark-inline-editing) {
    background: var(--bookmark-row-dimmed-bg);
    --bookmark-row-edge: var(--bookmark-row-edge-dimmed);
}

body.bookmark-kbd-selection-dimmed .bookmark-link.keyboard-selected:not(:hover):not(.bookmark-inline-editing) .bookmark-superscript-badge,
body.bookmark-kbd-selection-dimmed .bookmark-link.keyboard-selected:not(:hover):not(.bookmark-inline-editing) .status-text,
body.bookmark-kbd-selection-dimmed .bookmark-link.keyboard-selected:not(:hover):not(.bookmark-inline-editing) .bookmark-shortcut {
    background: color-mix(in srgb, var(--background-secondary) 80%, transparent);
    border-color: color-mix(in srgb, var(--border-primary) 65%, transparent);
    color: var(--text-tertiary);
}

/* Status rows — explicit interaction chrome (wins over idle status tint / color-lock) */
.bookmarks-list[data-bookmarks-list="true"]:not([data-smart-collection="true"]) .bookmark-link:is(.status-online, .status-offline, .status-checking):is(:hover, .keyboard-selected, :focus-within):not(.bookmark-inline-editing) {
    color: var(--text-primary);
    background: var(--bookmark-row-hover-bg);
    --bookmark-row-edge: var(--bookmark-row-edge-hover);
    box-shadow: none;
}

.bookmarks-list[data-bookmarks-list="true"]:not([data-smart-collection="true"]) .bookmark-link:is(.status-online, .status-offline, .status-checking):is(.keyboard-selected, :focus-within):not(.bookmark-inline-editing) {
    background: var(--bookmark-row-selected-bg);
    --bookmark-row-edge: var(--bookmark-row-edge-selected);
}

/* ─── Shortcut label: the three display modes ─────────────────────────────── */
/*
 * The row always builds the label when the bookmark has a shortcut; which of
 * the three modes is in force is body[data-shortcut-display], so switching is
 * a repaint. "always" is the base styling elsewhere and needs nothing here.
 */

body[data-shortcut-display="never"] .bookmark-shortcut {
    display: none;
}

/*
 * "hover" draws the label as a pseudo-element of the bookmark name, and takes
 * the real one out of the row entirely. Two things follow from that, and both
 * are the point of the mode:
 *
 * The row's shortcut track has no item left in it, so it collapses and the name
 * gets that width -- roughly five characters a row, which is where the
 * truncated names came from.
 *
 * And the label lands exactly where the name's column ends. With response times
 * switched on that is short of the number, which owns the next column; with
 * them off it is the row's right edge. Nothing here has to know which, because
 * the label is a child of the name and `right: 0` means the name's own edge.
 *
 * The alternative -- one absolutely positioned label offset from the row's
 * right by the padding, the gap and the width of the status column -- was tried
 * and is a trap twice over. The column token is 3.35em and em on the label is
 * its own 0.68rem, so the offset came out a third short and the label sat on
 * top of the milliseconds; and even once corrected, the sum quietly stops being
 * true the first time any of those three change.
 */
body[data-shortcut-display="hover"] .bookmarks-list .bookmark-link > .bookmark-shortcut {
    display: none;
}

body[data-shortcut-display="hover"] .bookmarks-list .bookmark-link > .bookmark-open {
    position: relative;
}

/*
 * The padding-left is the fade runway, not spacing: the name is truncated to
 * this column's edge, so the label sits on its last characters and the gradient
 * dissolves one into the other rather than butting them together. It is painted
 * in the row's own edge colour -- see --bookmark-row-edge above -- so a hovered
 * or selected row fades into what it actually is there.
 */
body[data-shortcut-display="hover"] .bookmarks-list .bookmark-link > .bookmark-open[data-shortcut]::after {
    content: attr(data-shortcut);
    position: absolute;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    padding: 0.1em 0.4em 0.1em 1.75rem;
    font-family: var(--font-family-mono, monospace);
    font-size: var(--text-2xs, 0.68rem);
    font-weight: var(--font-weight-normal);
    line-height: 1.6;
    letter-spacing: 0.03em;
    color: var(--text-tertiary);
    background: linear-gradient(90deg, transparent 0, var(--bookmark-row-edge) 1.75rem);
    white-space: nowrap;
    pointer-events: none;
    user-select: none;
    opacity: 0;
    transition: opacity 0.12s ease;
}

body[data-shortcut-display="hover"] .bookmarks-list .bookmark-link:is(:hover, .keyboard-selected, :focus-within):not(.bookmark-inline-editing) > .bookmark-open[data-shortcut]::after {
    opacity: 1;
    color: var(--text-secondary);
}

body.no-animations[data-shortcut-display="hover"] .bookmarks-list .bookmark-link > .bookmark-open[data-shortcut]::after {
    transition: none;
}


/* ── Waiting on a bookmark ───────────────────────────────────────────────────
   Opening in the same tab leaves nextDash on screen until the other end
   answers. The row that was pressed says so: its icon turns into a spinner and
   the word sits at the end of the row. Drawn only after 150ms -- see
   markBookmarkOpening() in dashboard-visual.js -- so a bookmark that opens at
   once never flickers.
   ────────────────────────────────────────────────────────────────────────── */

.bookmark-link.is-opening .bookmark-icon-slot > * {
    opacity: 0.12;
}

.bookmark-link.is-opening .bookmark-icon-slot {
    position: relative;
}

.bookmark-link.is-opening .bookmark-icon-slot::after {
    content: '';
    position: absolute;
    inset: 50% auto auto 50%;
    width: 13px;
    height: 13px;
    margin: -7px 0 0 -7px;
    border: 2px solid color-mix(in srgb, var(--accent-primary) 35%, transparent);
    border-top-color: var(--accent-primary);
    border-radius: 50%;
    animation: bookmark-opening-spin 0.7s linear infinite;
}

.bookmark-opening-note {
    margin-left: auto;
    padding-left: var(--space-2, 0.5rem);
    color: var(--accent-primary);
    font-family: var(--font-family-main);
    font-size: var(--font-size-controls);
    font-weight: var(--font-weight-semibold);
    white-space: nowrap;
    flex: 0 0 auto;
}

@keyframes bookmark-opening-spin {
    to { transform: rotate(360deg); }
}

/* Reduced motion and the no-animations setting: the spinner stops turning and
   reads as a ring, which still says "this row is the one you are waiting on". */
body.no-animations .bookmark-link.is-opening .bookmark-icon-slot::after {
    animation: none;
}

@media (prefers-reduced-motion: reduce) {
    .bookmark-link.is-opening .bookmark-icon-slot::after {
        animation: none;
    }
}

/* ==== css/modal.css ==== */
/* Modal Styles - EXACT copy of shortcut-search */
/*
 * The scrim behind every modal: search, the omnibox, the bookmark form, app
 * dialogs, inbox triage and Health's walk-through. One pair of values, so
 * opening any of them dims and softens the page the same way.
 */
:root {
    --modal-scrim: rgba(0, 0, 0, 0.3);
    --modal-scrim-blur: 3px;
}

.modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    z-index: 2000;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.2s ease, visibility 0.2s ease;
    background: var(--modal-scrim);
    backdrop-filter: blur(var(--modal-scrim-blur));
    -webkit-backdrop-filter: blur(var(--modal-scrim-blur));
}

.modal-overlay.show {
    opacity: 1;
    visibility: visible;
}

.modal {
    padding: var(--space-5, 1.5rem) var(--space-6, 2rem);
    max-width: 400px;
    width: 90%;
    border: 1px solid color-mix(in srgb, var(--border-primary) 75%, transparent);
    border-radius: var(--layout-radius-md, 12px);
    background: var(--background-modal);
    box-shadow: 0 24px 64px rgba(0, 0, 0, 0.28), 0 2px 8px rgba(0, 0, 0, 0.12);
    overflow: hidden;
    transform: scale(0.98) translateY(8px);
    opacity: 0;
    transition: transform 0.2s ease, opacity 0.2s ease;
}

.modal-overlay.show .modal {
    transform: scale(1) translateY(0);
    opacity: 1;
}

/*
 * Out of the control that opened it.
 *
 * ModalOrigin (modal.js) writes the offset from the panel's centre to the
 * button's; the panel plays that distance back in 160ms. A keyframe rather
 * than a transition: the class and `.show` land in the same tick, and a
 * transition started from a state the browser never painted does nothing at
 * all -- which is what "no animation" looked like.
 */
@keyframes modal-from-origin {
    from {
        transform:
            translate(var(--modal-origin-x, 0), var(--modal-origin-y, 0))
            scale(0.4);
        opacity: 0;
    }
    to {
        transform: translate(0, 0) scale(1);
        opacity: 1;
    }
}

.modal-overlay.show .modal.from-origin {
    animation: modal-from-origin 0.16s cubic-bezier(0.16, 1, 0.3, 1) both;
    transform-origin: center;
}

/* Reduced motion and the no-animations setting: the panel simply appears where
   it belongs. The origin is a nicety, and a nicety is the first thing to go. */
body.no-animations .modal-overlay.show .modal.from-origin {
    animation: none;
}

@media (prefers-reduced-motion: reduce) {
    .modal-overlay.show .modal.from-origin {
        animation: none;
    }
}

/* 36rem, not the 52rem the cheat sheet uses: this modal is one column of prose,
   and at 52rem its lines ran to ~84 characters — well past the 45–75 where the
   eye still finds the next line reliably. It sat at 40rem (~61 characters) and
   came down again once the release stopped competing with three panels above
   it; 36rem puts a line at ~58. The cheat sheet keeps 52rem because it is a
   two-column key table that needs the width. */
.modal.whats-new-modal {
    max-height: 90vh;
    max-width: min(36rem, calc(100vw - 2.5rem));
    width: min(36rem, calc(100vw - 2.5rem));
    overflow: hidden;
    display: flex;
    flex-direction: column;
}

.modal.whats-new-modal .modal-body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding-right: var(--space-1, 0.25rem);
}

/* Config → Help: same What's new shell as dashboard toolbar modals (centered) */
body:has(#config-main) #app-modal.modal-overlay:has(.whats-new-modal) {
    justify-content: center;
    align-items: center;
    padding: max(1.25rem, 2vw);
    box-sizing: border-box;
    backdrop-filter: blur(var(--modal-scrim-blur));
    -webkit-backdrop-filter: blur(var(--modal-scrim-blur));
}

body:has(#config-main) #app-modal.modal-overlay:has(.whats-new-modal) .modal {
    width: min(36rem, calc(100vw - 2.5rem));
    max-width: 100%;
    margin: 0;
    max-height: min(70vh, calc(100vh - 4rem));
    transform: translateY(10px) scale(0.98);
    transition: transform 0.15s ease-in, opacity 0.15s ease-in;
}

/* The release notes themselves, not the shells that borrow the class: wide
   and tall at a fixed size, so new changes sit side by side as cards and the
   modal does not jump when the section menu narrows the list. The lines stay
   short because the cards and the two columns of fixes divide the width. */
body:has(#config-main) #app-modal.modal-overlay:has(.whats-new-modal:not(.analytics-notice-modal)) .modal {
    width: min(64rem, calc(100vw - 2.5rem));
    height: min(52rem, calc(100vh - 4rem));
    max-height: calc(100vh - 4rem);
}

body:has(#config-main) #app-modal.modal-overlay.show:has(.whats-new-modal) .modal {
    transform: translateY(0) scale(1);
    transition: transform 0.38s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.22s ease-out;
}

body.no-animations:has(#config-main) #app-modal.modal-overlay:has(.whats-new-modal) .modal {
    transform: none !important;
    transition: opacity 0.15s ease !important;
}

.wn-skeleton-stack {
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 0.55rem);
    margin-top: var(--space-2, 0.5rem);
}

.wn-skeleton-line {
    height: 0.72rem;
    border-radius: var(--radius-3, 0.25rem);
    background: linear-gradient(
        90deg,
        color-mix(in srgb, var(--border-primary) 55%, transparent) 0%,
        color-mix(in srgb, var(--border-primary) 25%, transparent) 50%,
        color-mix(in srgb, var(--border-primary) 55%, transparent) 100%
    );
    background-size: 200% 100%;
    animation: wnSkeletonShimmer 1.2s ease-in-out infinite;
}

.wn-skeleton-line--short {
    width: 62%;
}

.wn-empty {
    margin: var(--space-2, 0.5rem) 0;
    color: var(--text-secondary);
    line-height: 1.55;
}

.wn-release-loading {
    margin: var(--space-4, 1rem) 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 0.45rem);
}

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

body.no-animations .wn-skeleton-line {
    animation: none;
}

/* ── What's New intro block ─────────────────────────────────────── */
/* One column at every width: the release summary reads as a paragraph, and
   side by side with the Ko-fi block it was squeezed into a narrow column while
   the shorter block left dead space beside it.

   The two blocks are fixed overhead on every open, sitting in the most valuable
   space in the modal, so the spacing around them is kept tight — 1rem + 1.5rem
   pushed the first actual release item below the fold. */
@media (max-width: 42rem) {
    .modal.whats-new-modal .modal-header {
        flex-wrap: wrap;
        align-items: flex-start;
    }

    /* The footer's two halves stop fitting side by side long before the modal
       runs out of room; stacked they still read in the same order. */
    .wn-foot {
        flex-direction: column;
        align-items: flex-start;
    }
}

.modal.whats-new-modal .modal-header {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3, 0.65rem) var(--space-4, 1rem);
    flex-wrap: nowrap;
}

.modal.whats-new-modal .modal-header .modal-title {
    flex: 0 0 auto;
    width: auto;
    margin-right: 0;
}

/* Lives in the footer now, so it reads from the left like the text above it
   rather than right-aligned across the header. */
.wn-update-check-header {
    width: 100%;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    gap: var(--space-2, 0.55rem);
    min-width: 0;
}

/* An available update is worth marking, but a tinted bordered box in the
   footer would put back exactly the panel this redesign removed from the top.
   A dot in the accent is enough to catch the eye of somebody who has finished
   reading. */
.wn-update-check-header--warn::before {
    content: "";
    flex: 0 0 auto;
    width: 0.42rem;
    height: 0.42rem;
    border-radius: 50%;
    background: var(--accent-primary);
}

.wn-update-check-meta {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    flex-wrap: wrap;
    gap: var(--space-1-5, 0.35rem) var(--space-2, 0.45rem);
    min-width: 0;
    flex: 1 1 auto;
}

.wn-update-check-dismiss {
    padding: var(--space-hairline, 0.15rem) var(--space-2, 0.45rem);
    border: none;
    border-radius: var(--radius-4, 0.35rem);
    background: transparent;
    color: var(--text-secondary);
    font: inherit;
    font-size: calc(var(--font-size-controls) * 0.92);
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 0.12em;
}

.wn-update-check-dismiss:hover {
    color: var(--text-primary);
}

/* Wraps rather than truncates.

   It was one nowrap line with an ellipsis, which fitted while it had the whole
   header to itself. Beside the Ko-fi button in a narrower modal it started
   cutting off mid-sentence — and "Could not reach GitHub. Try again lat…" is a
   message nobody can act on. */
.wn-update-check-status {
    margin: 0;
    font-size: calc(var(--font-size-controls) * 0.94);
    color: var(--text-secondary);
    line-height: 1.45;
    text-align: left;
    white-space: normal;
    max-width: 100%;
}

.wn-update-check-status--warn { color: var(--accent-primary); }
.wn-update-check-status--ok { color: var(--text-secondary); }
.wn-update-check-status--error { color: var(--accent-error, #d1493f); }
.wn-update-check-status--loading { color: var(--text-secondary); font-style: italic; }

.wn-update-check-link {
    display: inline-block;
    margin-top: 0;
    font-size: var(--font-size-controls);
    color: var(--accent-primary);
    text-decoration: none;
    white-space: nowrap;
    flex-shrink: 0;
}

.wn-update-check-link:hover { text-decoration: underline; }

.wn-kofi-btn {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1-5, 0.4rem);
    padding: var(--space-1, 0.3rem) var(--space-3, 0.75rem);
    background: color-mix(in srgb, var(--accent-primary) 13%, transparent);
    color: var(--accent-primary);
    border: 1px solid color-mix(in srgb, var(--accent-primary) 32%, transparent);
    border-radius: var(--radius-4, 0.4rem);
    text-decoration: none;
    font-size: var(--font-size-controls);
    font-family: var(--font-family-main);
    font-weight: var(--font-weight-normal);
    white-space: nowrap;
    transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
    flex-shrink: 0;
}

.wn-kofi-btn:hover {
    background: color-mix(in srgb, var(--accent-primary) 22%, transparent);
    border-color: color-mix(in srgb, var(--accent-primary) 55%, transparent);
    transform: translateY(-1px);
}

.wn-kofi-btn:active {
    transform: translateY(0);
}

.wn-kofi-icon,
.wn-kofi-label {
    position: relative;
    z-index: 1;
    flex-shrink: 0;
}

/* What's new modal — animated donate CTA with twinkling stars */
.wn-kofi-btn--animated {
    position: relative;
    overflow: visible;
    isolation: isolate;
    animation: wn-kofi-btn-glow 3.2s ease-in-out infinite;
}

.wn-kofi-btn--animated::before {
    content: '';
    position: absolute;
    inset: -1px;
    border-radius: inherit;
    background: linear-gradient(
        105deg,
        transparent 35%,
        color-mix(in srgb, var(--accent-primary) 28%, transparent) 50%,
        transparent 65%
    );
    background-size: 220% 100%;
    animation: wn-kofi-btn-shimmer 3.2s ease-in-out infinite;
    pointer-events: none;
    z-index: 0;
    opacity: 0.85;
}

.wn-kofi-stars {
    position: absolute;
    inset: -0.55rem -0.75rem;
    pointer-events: none;
    z-index: 2;
}

.wn-kofi-star {
    position: absolute;
    line-height: 1;
    color: color-mix(in srgb, var(--accent-primary) 88%, #fff 12%);
    text-shadow: 0 0 6px color-mix(in srgb, var(--accent-primary) 45%, transparent);
    opacity: 0;
    transform: scale(0.5) rotate(0deg);
    animation: wn-kofi-star-twinkle 2.6s ease-in-out infinite;
}

.wn-kofi-star::before {
    content: '★';
    display: block;
    font-size: var(--text-2xs, 0.62rem);
}

.wn-kofi-star:nth-child(1) {
    top: -0.1rem;
    left: 6%;
    animation-delay: 0s;
}

.wn-kofi-star:nth-child(2) {
    top: -0.35rem;
    right: 10%;
    animation-delay: 0.65s;
}

.wn-kofi-star:nth-child(2)::before {
    content: '✦';
    font-size: 0.5rem;
}

.wn-kofi-star:nth-child(3) {
    bottom: -0.15rem;
    left: 18%;
    animation-delay: 1.3s;
}

.wn-kofi-star:nth-child(3)::before {
    font-size: 0.48rem;
}

.wn-kofi-star:nth-child(4) {
    bottom: 0;
    right: 4%;
    animation-delay: 1.95s;
}

.wn-kofi-star:nth-child(4)::before {
    content: '✦';
    font-size: 0.54rem;
}

@keyframes wn-kofi-star-twinkle {
    0%, 100% {
        opacity: 0;
        transform: scale(0.45) rotate(-12deg);
    }
    45%, 55% {
        opacity: 1;
        transform: scale(1.15) rotate(10deg);
    }
}

@keyframes wn-kofi-btn-glow {
    0%, 100% {
        box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent-primary) 0%, transparent);
    }
    50% {
        box-shadow: 0 0 0.65rem 0.12rem color-mix(in srgb, var(--accent-primary) 32%, transparent);
    }
}

@keyframes wn-kofi-btn-shimmer {
    0%, 100% {
        background-position: 120% 0;
    }
    50% {
        background-position: -20% 0;
    }
}

.wn-kofi-btn--animated:hover {
    animation: none;
    box-shadow: 0 0 0.75rem 0.15rem color-mix(in srgb, var(--accent-primary) 38%, transparent);
}

.wn-kofi-btn--animated:hover .wn-kofi-star {
    animation-duration: 1.4s;
}

body.no-animations .wn-kofi-btn--animated,
body.no-animations .wn-kofi-btn--animated::before,
body.no-animations .wn-kofi-star {
    animation: none;
}

body.no-animations .wn-kofi-btn--animated {
    box-shadow: 0 0 0.5rem 0.1rem color-mix(in srgb, var(--accent-primary) 28%, transparent);
}

body.no-animations .wn-kofi-star {
    opacity: 0.75;
    transform: scale(1);
}

/* ── What's New release blocks ─────────────────────────────────── */
/* ===========================================================================
   The release itself.

   Rewritten from a run of near-identical rows into three levels the eye can
   tell apart without reading: one release headline, a group per area, and an
   entry that is a title with an explanation under it.

   The measurement that drove it: 283px of the 918px modal was spent before the
   first release, on three bordered panels that were not about the release —
   and six separate elements shared the accent colour, so nothing it marked
   stood out. Panels are gone; the accent now marks two things.
   =========================================================================== */

.wn-hero {
    padding-bottom: var(--space-4, 1.1rem);
    margin-bottom: var(--space-1-5, 0.4rem);
    border-bottom: 1px solid var(--border-primary);
}

/* The version, set as the headline it is. Nothing else in the modal is this
   size, which is what makes "which release am I reading" answerable at a
   glance rather than by finding a small chip. */
.wn-hero-version {
    margin: 0;
    font-size: calc(var(--font-size-text) * 1.75);
    font-weight: var(--font-weight-semibold);
    line-height: 1.1;
    letter-spacing: -0.01em;
    color: var(--text-primary);
}

.wn-hero-meta {
    margin: var(--space-1, 0.2rem) 0 0;
    font-size: var(--font-size-controls);
    color: var(--text-tertiary);
    letter-spacing: 0.02em;
}

/* modalLead, written per release and previously boxed. A subtitle is what it
   always was. */
.wn-hero-lead {
    margin: var(--space-3, 0.75rem) 0 0;
    font-size: var(--font-size-text);
    line-height: 1.55;
    color: var(--text-primary);
    max-width: 46ch;
}

.wn-group, .wn-block { margin-top: var(--space-5, 1.5rem); }

/* An eyebrow rather than a heading: the group name is a label on what follows,
   and at heading weight it competed with the entry titles it introduces. */
.wn-group-title {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2, 0.6rem);
    margin: 0 0 var(--space-2, 0.6rem);
    padding-bottom: var(--space-1-5, 0.35rem);
    border-bottom: 1px solid var(--border-primary);
    font-size: calc(var(--font-size-controls) * 0.86);
    font-weight: var(--font-weight-semibold);
    letter-spacing: 0.06em;
    /* The app lower-cases everywhere it names something; this shouted. */
    text-transform: lowercase;
    color: var(--text-tertiary);
}
.wn-group-count {
    letter-spacing: 0.04em;
    opacity: 0.75;
    font-variant-numeric: tabular-nums;
}

.wn-entries {
    list-style: none;
    padding: 0;
    margin: 0;
}

.wn-entry {
    display: grid;
    grid-template-columns: 2.4rem 1fr;
    gap: 0 var(--space-2, 0.6rem);
    padding: var(--space-2, 0.5rem) 0;
    align-items: start;
}
.wn-entry + .wn-entry {
    border-top: 1px solid color-mix(in srgb, var(--border-primary) 55%, transparent);
}

/* The word, not a dot.

   A filled or hollow dot stood here and was read by nobody: it says "these two
   entries differ" without saying how, and the reader has to be told once what
   the difference is before it means anything. It replaced a chip for two
   reasons, and both are answered rather than reverted.

   Width: the chip was uppercase at body size and cost about seven characters
   of a column deliberately kept to ~63. This one is 0.62rem, tabular-width, in
   a fixed 2.4rem track — a hair over three characters at the column's own size,
   and the text beside it starts at the same place on every row.

   Colour: "new" drew on --accent-success, which on a green accent is the same
   value as --accent-primary — so the badge and the version tag beside it were
   literally the same colour. --accent-info is no escape: on the terminal theme
   it resolves to the same value again. So neither badge takes an accent at
   all. New is the page's own foreground on a solid fill, fix is muted text in
   a hairline outline. Filled against outlined reads at a glance on any theme,
   and neither can collide with a hue it does not use. */
.wn-badge {
    justify-self: start;
    margin-top: var(--space-1-5, 0.34rem);
    padding: var(--space-hairline, 0.08rem) var(--space-1, 0.3rem);
    border-radius: var(--radius-3, 0.25rem);
    font-size: var(--text-2xs, 0.62rem);
    font-weight: var(--font-weight-semibold);
    line-height: 1.5;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.wn-badge--new {
    color: var(--background-primary);
    background: var(--text-primary);
    border: 1px solid var(--text-primary);
}
.wn-badge--fix {
    color: var(--text-tertiary);
    background: transparent;
    border: 1px solid var(--border-primary);
}

.wn-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;
}

.wn-entry-main { min-width: 0; }

/* The lead sentence, on its own line. It was bold-inside-a-paragraph before —
   ten to twenty bold words, which is long enough that bold stops reading as
   emphasis and the explanation behind it ran straight on. */
.wn-entry-title {
    font-size: var(--font-size-text);
    font-weight: var(--font-weight-semibold);
    line-height: 1.45;
    color: var(--text-primary);
}

.wn-entry-body {
    margin-top: var(--space-hairline, 0.15rem);
    font-size: calc(var(--font-size-text) * 0.95);
    line-height: 1.55;
    color: var(--text-secondary);
    opacity: 0.92;
}
/* Folded at three lines. A release of 110 items is one long scroll however it
   is set; this makes the scroll a list of headlines with detail on request. */
.wn-entry-body.is-folded {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.wn-entry-body strong { color: var(--text-primary); }
.wn-entry-body code,
.wn-entry-title code {
    font-family: var(--font-family-main);
    font-size: 0.88em;
    background: var(--background-secondary);
    border: 1px solid var(--border-primary);
    padding: var(--space-hairline, 0.05rem) var(--space-1, 0.28rem);
    color: var(--text-primary);
}

.wn-entry-more {
    margin-top: var(--space-1, 0.2rem);
    padding: 0;
    background: none;
    border: 0;
    border-bottom: 1px dotted currentColor;
    font-family: inherit;
    font-size: calc(var(--font-size-text) * 0.88);
    color: var(--text-tertiary);
    cursor: pointer;
}
.wn-entry-more:hover { color: var(--accent-primary); }

/* Shortcuts keep the warning accent, which is the one place a second colour
   still earns its keep: a key you can press is a different kind of thing from
   a change you can read about. */
.wn-group--keys .wn-group-title { color: var(--accent-warning); }
.wn-entry--keys {
    grid-template-columns: auto 1fr;
    align-items: baseline;
    gap: 0 var(--space-2, 0.55rem);
}
.wn-keycap {
    flex-shrink: 0;
    font-family: var(--font-family-main);
    font-size: 0.82em;
    font-weight: var(--font-weight-semibold);
    color: var(--accent-warning);
    background: color-mix(in srgb, var(--accent-warning) 12%, var(--background-primary));
    border: 1px solid color-mix(in srgb, var(--accent-warning) 45%, transparent);
    border-radius: var(--layout-radius-sm, 5px);
    padding: var(--space-hairline, 0.1rem) var(--space-1-5, 0.4rem);
    white-space: nowrap;
    min-width: 2.2rem;
    text-align: center;
}
.wn-entry--keys .wn-entry-title { font-weight: var(--font-weight-normal, 400); }

/* ---- Everything older, one row each ------------------------------------- */

.wn-earlier {
    margin-top: var(--space-6, 2rem);
    padding-top: var(--space-4, 1.1rem);
    border-top: 1px solid var(--border-primary);
}

.wn-earlier-title {
    margin: 0 0 var(--space-1-5, 0.4rem);
    font-size: calc(var(--font-size-controls) * 0.86);
    font-weight: var(--font-weight-semibold);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}

.wn-earlier-list {
    list-style: none;
    padding: 0;
    margin: 0;
}

.wn-earlier-row {
    display: flex;
    align-items: baseline;
    gap: var(--space-2, 0.6rem);
    width: 100%;
    padding: var(--space-2, 0.45rem) 0;
    background: none;
    border: 0;
    border-bottom: 1px solid color-mix(in srgb, var(--border-primary) 55%, transparent);
    font-family: inherit;
    font-size: var(--font-size-controls);
    color: var(--text-secondary);
    text-align: left;
    cursor: pointer;
}
.wn-earlier-row:hover { color: var(--text-primary); }
.wn-earlier-tag {
    font-weight: var(--font-weight-semibold);
    color: var(--text-primary);
    white-space: nowrap;
}
.wn-earlier-date {
    flex: 1;
    color: var(--text-tertiary);
    min-width: 0;
}
.wn-earlier-chevron {
    color: var(--text-tertiary);
    transition: transform 0.15s ease;
}
.wn-earlier-row[aria-expanded="true"] .wn-earlier-chevron {
    transform: rotate(90deg);
}
body.no-animations .wn-earlier-chevron { transition: none; }

/* An opened release is indented behind a rule, so where it starts and where
   the next row begins is a matter of looking rather than counting. This is the
   one place the old left-hand accent rule still earns its keep: it marks a
   containment that is really there. */
.wn-earlier-body {
    padding: var(--space-hairline, 0.1rem) 0 var(--space-4, 1rem) var(--space-4, 0.9rem);
    margin-left: var(--space-hairline, 0.15rem);
    border-left: 2px solid color-mix(in srgb, var(--accent-primary) 35%, transparent);
}
.wn-earlier-body .wn-group:first-child { margin-top: var(--space-3, 0.8rem); }
.wn-earlier-body .wn-group-title { border-bottom-color: color-mix(in srgb, var(--border-primary) 70%, transparent); }

/* ---- The footer, where the asking happens ------------------------------- */

/* Sticky so it is always reachable without ever being the first thing read.
   Both halves used to be bordered panels above the first release: the update
   status across the header in the accent colour, and the support request
   directly under it. */
.wn-foot {
    position: sticky;
    bottom: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2, 0.6rem) var(--space-4, 1rem);
    flex-wrap: wrap;
    margin-top: var(--space-5, 1.6rem);
    padding: var(--space-2, 0.6rem) 0 var(--space-1, 0.2rem);
    border-top: 1px solid var(--border-primary);
    background: var(--background-primary);
}
/* Lifted out of the scroll area (liftFooter in whats-new-modal.js): nothing
   scrolls under it any more, so it is neither sticky nor painted. */
.wn-foot.wn-foot--lifted {
    position: static;
    flex: 0 0 auto;
    margin-top: var(--space-3, 0.75rem);
    background: none;
}
.wn-foot-update { min-width: 0; flex: 1 1 12rem; }
/* Stacked on a narrow screen, the 12rem basis meant for the row became a
   height: a modal of fixed height lost it from the notes. */
@media (max-width: 42rem) {
    .wn-foot-update { flex-basis: auto; }
}
.wn-foot-update:empty { display: none; }

.modal.keyboard-cheat-sheet-modal {
    max-width: min(52rem, calc(100vw - 2.5rem));
    width: min(52rem, calc(100vw - 2.5rem));
    margin: auto;
    max-height: 90vh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    padding: var(--space-4, 1rem) 1.2rem;
}

.modal.keyboard-cheat-sheet-modal .modal-body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding-right: var(--space-1, 0.25rem);
}

/* Page overview — compact centered modal (dashboard header pages button) */
.modal.page-overview-modal {
    max-width: min(26rem, calc(100vw - 2.5rem));
    width: min(26rem, calc(100vw - 2.5rem));
    margin: auto;
    max-height: min(70vh, calc(100vh - 4rem));
    overflow: hidden;
    display: flex;
    flex-direction: column;
    padding: var(--space-4, 1rem) var(--space-4, 1.15rem);
}

.modal.page-overview-modal .modal-body {
    flex: 1;
    min-height: 0;
    /*
     * One scrollbar, and it belongs to the rows.
     *
     * The list scrolls itself (max-height below), so a scrolling body put a
     * second bar beside the first the moment the pages outgrew the panel --
     * two tracks for one list, and the outer one moved the filter and the foot
     * out of view along with it.
     *
     * A column, so the parts that must stay -- the filter, the new-page entry
     * and the foot -- keep their height and the rows take whatever is left.
     */
    overflow: hidden;
    display: flex;
    flex-direction: column;
    padding-right: var(--space-1, 0.2rem);
    /* The last child ends flush with this scroll box, and overflow clipping eats
       the final pixel row — the name input's bottom border went missing. A pixel
       of padding gives that border somewhere to land. */
    padding-bottom: 1px;
    margin-top: var(--space-2, 0.55rem);
    margin-bottom: var(--space-1, 0.25rem);
}

/*
 * One slab holding the rows, the way the recents panel holds its own.
 *
 * Every page was an outlined card with a gap under it, so a dozen pages read
 * as a dozen objects rather than as one list -- and the boxes competed with
 * the panel's own edge. The surface comes from the shared overlay-slab rule
 * (see the :is() list below, which the recents list and the cheat sheet's
 * groups already stand on); only the layout is here.
 */
/* AppModal wraps htmlMessage in .modal-text, so the column passes through it:
   without this the slab is laid out inside a block of content height and the
   body overflows however tall the panel is. */
.modal.page-overview-modal .modal-body > .modal-text {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

/*
 * One slab, holding the rows and the way to make another page.
 *
 * The surface used to sit on the list itself, so the new-page entry stood on a
 * block of its own under it -- a gap, a second set of corners and a second
 * shadow for one more line. The slab is the wrapper now; the list scrolls
 * inside it and the entry sits flush under the last row.
 */
.page-overview-modal-slab {
    display: flex;
    flex-direction: column;
    /* Takes the room the filter and the foot leave, and gives it to the rows. */
    flex: 1 1 auto;
    min-height: 0;
    padding: var(--space-2, 0.5rem);
    overflow: hidden;
}

.page-overview-modal-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0;
    /* No fixed ceiling: the panel's own max-height decides how tall it gets,
       and the rows scroll inside whatever that leaves. A 52vh cap here meant
       the body overflowed on a short window and grew a second scrollbar. */
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
}

.page-overview-modal-item {
    margin: 0;
    padding: 0;
    /* One line between rows rather than a gap: a gap makes every row an object, a
       rule makes them a list. */
    border-top: 1px solid var(--hairline, var(--border-primary));
}

.page-overview-modal-item:first-child,
.page-overview-modal-item[hidden] + .page-overview-modal-item {
    border-top: none;
}

.page-overview-modal-link {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: 0.6rem;
    width: 100%;
    margin: 0;
    /* 52px of row: a list you aim at with a pointer, one vertical run of
       targets, no diagonal travel between two columns -- and room for the name
       at the size everything else on the page is read at. */
    min-height: 52px;
    padding: 0.6rem 0.65rem;
    border: none;
    border-radius: var(--radius-3, 4px);
    background: transparent;
    cursor: pointer;
    font: inherit;
    font-size: var(--font-size-controls);
    text-align: left;
    color: var(--text-primary);
    /* The name is the thing being chosen, so it is read rather than scanned. */
    font-size: var(--font-size-text);
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
    transition: background 0.1s ease, color 0.1s ease;
}

/*
 * Hover and the keyboard land on the same row, and the focused one carries
 * the accent bar on its left edge -- the same treatment the recents rows take,
 * because choosing a page and choosing a bookmark are the same gesture.
 */
.page-overview-modal-link:hover,
.page-overview-modal-link:focus-visible,
.page-overview-modal-item.is-focused .page-overview-modal-link {
    background: color-mix(in srgb, var(--accent-primary) 12%, transparent);
    outline: none;
}

.page-overview-modal-link:focus-visible,
.page-overview-modal-item.is-focused .page-overview-modal-link {
    box-shadow:
        inset 3px 0 0 var(--accent-primary),
        0 0 12px color-mix(in srgb, var(--accent-primary) calc(28% * var(--theme-depth, 1)), transparent);
}

.page-overview-modal-item.is-current .page-overview-modal-name {
    color: var(--accent-primary);
    font-weight: var(--font-weight-semibold);
}

/*
 * The page you are on, said on the row rather than only in its name.
 *
 * The focused row already carries an accent bar on its left edge; this is the
 * same mark held permanently for the current page, with the wash behind it, so
 * "where I am" survives walking the list with the arrows.
 */
.page-overview-modal-item.is-current .page-overview-modal-link {
    background: color-mix(in srgb, var(--accent-primary) 10%, transparent);
    box-shadow: inset 2px 0 0 var(--accent-primary);
}

.page-overview-modal-item[hidden] {
    display: none;
}

/* A fixed column for the number, left aligned: the names then start on one
   vertical line whether a page is 1 or 10. */
.page-overview-modal-lead {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1, 0.25rem);
    width: 1.5rem;
    min-width: 1.5rem;
    justify-content: flex-start;
}

.page-overview-modal-num {
    font-size: 0.78em;
    color: var(--text-tertiary);
    font-variant-numeric: tabular-nums;
}

.page-overview-modal-body {
    min-width: 0;
}

.page-overview-modal-name {
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The count is a fact about the page, not part of its name: a pill holds it
   apart, the way a badge does elsewhere in the app. */
.page-overview-modal-count {
    padding: 0.1rem 0.45rem;
    border-radius: var(--radius-pill, 999px);
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
    font-size: 0.78em;
    color: var(--text-tertiary);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.page-overview-modal-item.is-current .page-overview-modal-count {
    background: color-mix(in srgb, var(--accent-primary) 18%, transparent);
    color: var(--text-primary);
}

/* ─── The filter, past eight pages ───────────────────────────────────────────
   Drawn only when the list is long enough to search rather than read; see
   PAGE_FILTER_FROM in dashboard-ui-helpers.js. It takes focus when the panel
   opens, so typing narrows at once. */

.page-overview-modal-filterbar {
    margin-bottom: var(--space-2, 0.5rem);
}

.page-overview-modal-filter {
    width: 100%;
    padding: 0.45rem 0.6rem;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-4, 6px);
    background: var(--background-primary);
    color: var(--text-primary);
    font: inherit;
    font-size: var(--font-size-controls);
}

.page-overview-modal-filter:focus-visible {
    outline: none;
    border-color: var(--accent-primary);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-primary) 25%, transparent);
}

.page-overview-modal-filter::placeholder {
    color: var(--text-tertiary);
}

/* ─── One page: the offer is the point ───────────────────────────────────────
   With a single page the panel is not a list to choose from, so the New page
   entry stops being a small row at the foot and becomes what the panel is for,
   with the one line that says what a page is. */

.page-overview-modal-actions.is-alone .page-overview-modal-new {
    justify-content: center;
    padding-block: var(--space-3, 0.75rem);
}

/* Share this page / Import a page: small, under New page -- a thing to do now and then. */
.page-overview-modal-files {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-1, 0.25rem) var(--space-3, 0.75rem);
    margin-top: var(--space-2, 0.5rem);
    /* Never what decides how wide the sheet is: the rows do that. */
    contain: inline-size;
}
.page-overview-modal-file {
    padding: 2px 4px;
    border: 0;
    background: none;
    color: var(--text-secondary);
    font: inherit;
    font-size: var(--font-size-controls);
    cursor: pointer;
    text-decoration: underline;
    text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
    text-underline-offset: 3px;
}
.page-overview-modal-file:hover,
.page-overview-modal-file:focus-visible { color: var(--text-primary); }

.page-overview-modal-newhint {
    display: block;
    margin-top: var(--space-2, 0.5rem);
    color: var(--text-tertiary);
    font-size: var(--font-size-controls);
    line-height: 1.4;
    text-align: center;
}

/* ─── Deleting a page — asked twice ──────────────────────────────────────────
   The cross sits at the end of the row, quiet until the row is hovered or the
   cursor is on it. Armed, the row turns the colour of the thing it is about to
   do and says what that is; the second press is the one that deletes. */

.page-overview-modal-item {
    position: relative;
}

/*
 * Big enough to read, and out of the count's way.
 *
 * It was a 1.4rem cross at 0.7 opacity sitting on top of the bookmark count --
 * two things in the same ten pixels, and the one you can press was the fainter
 * of them. The row keeps a lane for it on the right, and the cross is drawn at
 * full strength on its own plate.
 */
.page-overview-modal-item .page-overview-modal-link {
    padding-right: 2.1rem;
}

.page-overview-modal-delete {
    position: absolute;
    top: 50%;
    right: 0.35rem;
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.6rem;
    height: 1.6rem;
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--radius-3, 4px);
    background: transparent;
    color: var(--text-secondary);
    font-family: var(--font-family-main);
    font-size: 1.05rem;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
    /*
     * Out of sight until the row is the one being acted on.
     *
     * A cross on every row is a delete offered ten times over, and it sat
     * beside the count competing with it. Hover, the keyboard cursor and focus
     * each bring it back -- see the rule below -- so the pointer and the
     * keyboard reach it the same way.
     */
    /*
     * Out of sight until the row is the one being acted on, but never out of
     * reach: the box stays where it is and keeps taking the pointer, so aiming
     * at it is the same gesture whether or not it has faded in yet. Hiding it
     * with pointer-events made the target appear under the cursor mid-click.
     */
    opacity: 0;
    transition: color 0.12s ease, background 0.12s ease, border-color 0.12s ease;
}

.page-overview-modal-item:hover .page-overview-modal-delete,
.page-overview-modal-item.is-focused .page-overview-modal-delete,
.page-overview-modal-item.is-armed .page-overview-modal-delete,
.page-overview-modal-delete:focus-visible {
    opacity: 1;
    border-color: color-mix(in srgb, var(--accent-error) 45%, transparent);
    background: color-mix(in srgb, var(--accent-error) 10%, transparent);
    color: var(--accent-error);
}

.page-overview-modal-delete:hover,
.page-overview-modal-delete:focus-visible {
    opacity: 1;
    color: var(--accent-error);
    background: color-mix(in srgb, var(--accent-error) 14%, transparent);
    outline: none;
}

/* Armed: the row says what the next press takes with it, and the cross it is
   waiting on is filled rather than outlined. */
.page-overview-modal-item.is-armed .page-overview-modal-link {
    background: color-mix(in srgb, var(--accent-error) 12%, transparent);
    box-shadow: inset 3px 0 0 var(--accent-error);
}

/* Armed, the row grows a line of its own underneath -- so the cross stops
   riding the middle of the row and stays on the line it belongs to. */
.page-overview-modal-item.is-armed .page-overview-modal-delete {
    top: 0.3rem;
    transform: none;
    opacity: 1;
    border-color: var(--accent-error);
    background: var(--accent-error);
    color: var(--background-primary);
}

.page-overview-modal-confirm {
    margin: 0.1rem 0 0.25rem;
    /* Clear of the cross above it, which is what the next press lands on. */
    padding: 0 2.4rem 0 0.8rem;
    color: var(--accent-error);
    font-size: var(--font-size-small, 0.72rem);
    line-height: 1.35;
}

/* ─── "New page" — the overlay's one action ──────────────────────────────────
   A button, not another row: the list above holds places to go and this is a
   thing to do. It wears the accent chip the header key wears, so the two marks
   the overlay makes in the accent colour are the same object at two sizes. */

/* The last entry on the slab, and drawn as one of its rows: same height, same
   padding, the same 1.5rem lead column so the + stands where the page numbers
   do, and the same hairline above it. No padding of its own -- the slab already
   carries it, and a wrapper with room around it made the entry taller than
   every row above it. */
.page-overview-modal-actions {
    display: flex;
    padding: 0;
    border-top: 1px solid var(--hairline, var(--border-primary));
}

.page-overview-modal-actions.is-alone {
    border-top: none;
}

/* The same three columns a page row stands in -- lead, name, chip -- rather
   than a flex run, so the + lands under the numbers and the key lands under
   the counts instead of near them. */
.page-overview-modal-new {
    display: grid;
    grid-template-columns: 1.5rem minmax(0, 1fr) auto;
    align-items: center;
    gap: 0.6rem;
    width: 100%;
    /* Fixed, not a minimum: the chip and the label together were pushing the
       entry past the rows it stands under. */
    height: 52px;
    margin: 0;
    /* Right padding matches a row's: the rows keep 2.1rem clear for the delete
       cross, so a plain 0.65rem here put the key chip a cross-width further
       right than every count above it. */
    padding: 0 2.1rem 0 0.65rem;
    border: none;
    border-radius: var(--radius-3, 4px);
    /* No tint of its own: it is the last row of the list, and a filled chip
       under a run of plain rows read as a second object stuck to the panel. */
    background: transparent;
    color: var(--accent-primary);
    font: inherit;
    font-size: var(--font-size-text);
    font-weight: var(--font-weight-semibold, 600);
    cursor: pointer;
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
    transition: background 0.1s ease;
}

.page-overview-modal-new:hover,
.page-overview-modal-new:focus-visible {
    background: color-mix(in srgb, var(--accent-primary) 12%, transparent);
    outline: none;
}

.page-overview-modal-new:focus-visible {
    box-shadow: 0 0 12px color-mix(in srgb, var(--accent-primary) calc(28% * var(--theme-depth, 1)), transparent);
}

.page-overview-modal-new[hidden] {
    display: none;
}

/* The lead column, where the page numbers stand: same width, same edge. */
.page-overview-modal-plus {
    display: block;
    text-align: left;
    font-size: 1.05em;
    line-height: 1;
}

.page-overview-modal-newlabel {
    text-align: left;
}

/* The shortcut hint reads the way the foot's ↵ does, in the accent rather than
   the text colour because it sits inside the tinted chip -- and it takes the
   count pill's box, so the right-hand column has one edge rather than two. */
.page-overview-modal-hintkey {
    padding: 0.1rem 0.45rem;
    border-radius: var(--radius-pill, 999px);
    background: color-mix(in srgb, var(--accent-primary) 16%, transparent);
    font-size: 0.78em;
    opacity: 0.85;
}

/* The name row takes the button's place inside the same action block, so
   opening it does not change the modal's height. */
.page-overview-modal-actions .bookmark-inline-create {
    display: flex;
    /* The action block is a flex row for the button's sake; the name row that
       takes its place still wants the whole width. */
    flex: 1 1 100%;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1-5, 0.35rem);
}

.page-overview-modal-actions .bookmark-inline-create[hidden] {
    display: none;
}

.page-overview-modal-actions .bookmark-inline-create-input {
    flex: 1 1 8rem;
    min-width: 0;
    padding: var(--space-1-5, 0.4rem) var(--space-2, 0.5rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--layout-radius-sm, 8px);
    background: var(--background-primary);
    color: var(--text-primary);
    font: inherit;
    font-size: var(--font-size-controls);
}

/* Colour and size come from .bookmark-inline-conflict in dashboard.css; only
   the full-width placement in this flex row belongs here. */
.page-overview-modal-actions .bookmark-inline-conflict {
    flex: 1 0 100%;
    margin-top: 0;
}

/* Fix hidden attribute override */
.modal-overlay[hidden] {
    display: none !important;
    opacity: 0 !important;
    visibility: hidden !important;
}

.modal-header {
    display: flex;
    align-items: center;
    font-size: var(--font-size-bookmark);
    font-weight: var(--font-weight-semibold);
    justify-content: flex-start;
}

.modal-title {
    margin-right: var(--space-2, 0.5rem);
    color: var(--text-primary);
}

.modal-body {
    margin-top: var(--space-5, 1.5rem);
    margin-bottom: var(--space-5, 1.5rem);
}

/* New bookmark modal textarea */
.new-bookmark-textarea {
    width: 100%;
    min-height: 4rem;
    padding: var(--space-2, 0.5rem);
    font-size: var(--text-base, 0.95rem);
    resize: vertical;
    margin-top: var(--space-1-5, 0.35rem);
}

.modal-text {
    min-width: 2ch;
    color: var(--text-primary);
    display: block;
}

/* The existing bookmark inside the "already saved" dialog: a link, because
   knowing where a copy is only helps if you can go there from here. */
.duplicate-existing {
    margin-top: var(--space-2, 0.6rem);
}

.duplicate-existing a {
    color: var(--accent-primary);
    word-break: break-word;
}

.duplicate-existing-hint {
    display: block;
    font-size: var(--text-xs, 0.75rem);
    color: var(--text-secondary);
}

.danger {
    color: var(--accent-error);
    font-weight: var(--font-weight-bold);
}

.modal-actions {
    display: flex;
    flex-direction: row;
    gap: var(--space-4, 1rem);
    justify-content: center;
    align-items: center;
}

.modal-button {
    padding: var(--space-2, 0.5rem) var(--space-4, 1rem);
    cursor: pointer;
    transition: all 0.3s ease;
    border: 1px solid var(--border-primary);
    border-radius: var(--layout-radius-sm, 6px);
    background: transparent;
    text-align: center;
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 1;
    min-width: 0;
    color: var(--text-primary);
    font-family: var(--font-family-main);
    
    /* Remove tap highlight on mobile */
    -webkit-tap-highlight-color: transparent;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

.modal-button:hover,
.modal-button.selected {
    color: var(--text-primary);
    transform: translateX(4px);
}

.modal-button-prefix {
    font-weight: var(--font-weight-bold);
    margin-right: var(--space-4, 1rem);
    min-width: 3ch;
    font-size: var(--font-size-controls);
    color: var(--accent-success);
}

.modal-button-name {
    font-size: var(--font-size-text);
    font-weight: var(--font-weight-semibold);
}

/* Danger button styles (red) */
.modal-button.danger {
    font-weight: var(--font-weight-bold);
    border: 1px solid var(--accent-error);
    color: var(--accent-error);
}

.modal-button.danger:hover,
.modal-button.danger.selected {
    background-color: var(--accent-error);
    color: var(--background-primary);
    transform: translateX(4px);
}

body.no-animations .modal-overlay,
body.no-animations .modal,
body.no-animations .modal-button {
    transition: none !important;
}

body.no-animations .modal {
    transform: none !important;
    opacity: 1 !important;
}

body.no-animations .modal-button:hover,
body.no-animations .modal-button.selected,
body.no-animations .modal-button.danger:hover,
body.no-animations .modal-button.danger.selected {
    transform: none !important;
}

.modal-button.danger .modal-button-prefix {
    color: var(--accent-error);
}

/* ESC to close hint */
.modal-esc-hint {
    text-align: center;
    margin-top: var(--space-3, 0.75rem);
    font-size: var(--text-2xs, 0.68rem);
    color: var(--text-tertiary);
    opacity: 0.6;
    user-select: none;
    pointer-events: none;
}

.modal-esc-hint kbd {
    font-family: var(--font-family-main);
    font-size: var(--text-2xs, 0.65rem);
    font-weight: var(--font-weight-semibold);
    padding: var(--space-hairline, 0.1rem) var(--space-1-5, 0.32rem);
    border: 1px solid var(--border-primary);
    background: var(--background-secondary);
    color: var(--text-tertiary);
    border-radius: var(--radius-2, 3px);
    letter-spacing: 0.04em;
    line-height: 1;
    vertical-align: baseline;
}

.modal-esc-hint:empty {
    display: none;
}

@media (pointer: coarse) {
    .modal-esc-hint { display: none; }
}

/* Modal select styling */
.modal-select {
    width: 100%;
    padding: var(--space-2, 0.5rem);
    border: 1px solid var(--border-primary);
    background: var(--background-primary);
    color: var(--text-primary);
    font-family: inherit;
    font-size: var(--font-size-controls);
    margin-top: var(--space-2, 0.5rem);
}

/* Modal page list styling */
.modal-page-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 0.5rem);
    margin-top: var(--space-2, 0.5rem);
}

.modal-page-btn {
    padding: var(--space-2, 0.5rem);
    border: 1px solid var(--border-primary);
    background: var(--background-primary);
    color: var(--text-primary);
    cursor: pointer;
    text-align: left;
    font-family: inherit;
    font-size: var(--font-size-controls);
    transition: background-color 0.3s ease;
}

.modal-page-btn:hover:not(.current) {
    background-color: var(--background-secondary);
}

.modal-page-btn.current {
    opacity: 0.6;
    cursor: not-allowed;
}

/* ── Keyboard cheat sheet: accordion style ───────────────────────────────── */

/*
 * Two columns of groups, the way the overlays spec lays a panel out.
 *
 * Fifteen sections in one column made the sheet 968px tall on a 1000px window
 * -- the whole screen, for a reference you are meant to glance at. The groups
 * are boxes with their own ground, so they sit beside each other without a
 * rule between them, and the open one simply makes its own row taller.
 *
 * Grid rather than CSS columns: a <details> in a multi-column flow breaks
 * across the column boundary when it opens.
 */
.keyboard-cheat-sheet {
    width: 100%;
}

.cheat-sheet-groups {
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 0.5rem);
}

.cheat-sheet-filter {
    width: 100%;
    box-sizing: border-box;
    background: transparent;
    border: 1px solid var(--border-primary);
    border-radius: var(--layout-radius-sm, 8px);
    color: var(--text-primary);
    font-family: inherit;
    font-size: var(--text-sm, 0.82rem);
    margin-bottom: var(--space-3, 0.75rem);
    outline: none;
    padding: var(--space-1-5, 0.35rem) var(--space-3, 0.65rem);
    transition: border-color 0.12s;
}
.cheat-sheet-filter:focus {
    border-color: var(--accent-primary);
}
.cheat-sheet-filter::placeholder {
    color: var(--text-tertiary);
    opacity: 0.65;
}

.cheat-sheet-no-results {
    margin: var(--space-3, 0.75rem) 0 0;
    color: var(--text-tertiary);
    font-size: var(--text-base, 0.9rem);
}

/*
 * One line naming the view the sheet was opened from, above the filter. Only
 * shown outside the bookmarks default, where there is nothing to explain.
 */
.cheat-sheet-context-lead {
    margin: 0 0 var(--space-2, 0.6rem);
    font-size: var(--text-xs, 0.72rem);
    color: var(--text-secondary);
}

.cheat-sheet-group {
    border-bottom: 1px solid var(--border-primary);
}

/*
 * The section matching the current view. Marked rather than moved or isolated:
 * every other section stays in place and the filter still searches them all.
 */
.cheat-sheet-group--context > .cheat-sheet-group-title {
    border-left: 2px solid var(--accent-primary);
    padding-left: var(--space-2, 0.5rem);
    margin-left: -0.5rem;
    color: var(--accent-primary);
}

.cheat-sheet-group:last-child {
    border-bottom: none;
}

.cheat-sheet-group-title {
    list-style: none;
    cursor: pointer;
    margin-bottom: var(--space-1, 0.25rem);
    padding: var(--space-1, 0.25rem) 0;
    border-bottom: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
    font-size: var(--text-xs, 0.7rem);
    font-weight: var(--font-weight-bold);
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--text-tertiary);
    display: flex;
    align-items: center;
    gap: var(--space-1-5, 0.4rem);
    user-select: none;
    border-radius: var(--layout-radius-xs, 6px);
}

.cheat-sheet-group-title::-webkit-details-marker {
    display: none;
}

.cheat-sheet-group-title::before {
    content: '▶';
    font-size: 0.5rem;
    color: var(--text-tertiary);
    transition: transform 0.15s ease;
    display: inline-block;
}

.cheat-sheet-group[open] .cheat-sheet-group-title::before {
    transform: rotate(90deg);
}

.cheat-sheet-group-title:hover {
    color: var(--text-secondary);
}

/* Keyboard clarity: always show where focus is inside cheat sheet modal. */
.keyboard-cheat-sheet-modal .cheat-sheet-filter:focus-visible,
.keyboard-cheat-sheet-modal .cheat-sheet-group-title:focus-visible,
.keyboard-cheat-sheet-modal .modal-button:focus-visible {
    outline: 2px solid color-mix(in srgb, var(--accent-primary) 82%, #fff 18%);
    outline-offset: 2px;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-primary) 24%, transparent);
}

.keyboard-cheat-sheet-modal .cheat-sheet-group-title:focus-visible {
    color: var(--text-primary);
    background: color-mix(in srgb, var(--accent-primary) 12%, transparent);
}

.keyboard-cheat-sheet-table {
    width: 100%;
    border-collapse: collapse;
    margin-bottom: 0;
}

.keyboard-cheat-sheet-table td {
    padding: var(--space-1, 0.22rem) 0;
    border-bottom: 1px solid var(--border-primary);
    vertical-align: top;
    line-height: 1.35;
}

.keyboard-cheat-sheet-table tr:last-child td {
    border-bottom: none;
}

/*
 * What it does, then what to press.
 *
 * The key came first and took 40% of the row, so fifteen sections opened with
 * a column of keys and the reader looked up what each one was for. The spec
 * has the row read as a line -- the thing, then the key that does it -- which
 * is also the order the reader arrives in: they know what they want to do.
 */
.keyboard-cheat-sheet-keys {
    font-family: "Source Code Pro", monospace;
    font-size: var(--text-xs, 0.72rem);
    font-weight: var(--font-weight-bold);
    color: var(--accent-primary);
    white-space: nowrap;
    text-align: right;
    padding-left: var(--space-3, 0.75rem);
    width: 1%;
}

.keyboard-cheat-sheet-description {
    color: var(--text-secondary);
    font-size: var(--text-xs, 0.78rem);
    min-width: 0;
    overflow-wrap: anywhere;
    width: auto;
}

@media (prefers-reduced-motion: reduce) {
    /* Modal open/close → keep opacity fade, remove scale + translate.
       Not the header sheets: their transform is the translateX(-50%) that
       centres them on the strip, not motion, and they drop their animation in
       a block of their own (see .modal.header-sheet). */
    .modal:not(.header-sheet) { transform: none !important; transition: opacity 0.15s ease !important; }
    .modal-overlay.show .modal:not(.header-sheet) { transform: none !important; }
    .modal-button,
    .modal-button:hover,
    .modal-button.selected,
    .modal-button.danger:hover,
    .modal-button.danger.selected { transition: none !important; transform: none !important; }
    .wn-kofi-btn--animated,
    .wn-kofi-btn--animated::before,
    .wn-kofi-star {
        animation: none !important;
    }
    .wn-kofi-btn--animated {
        box-shadow: 0 0 0.5rem 0.1rem color-mix(in srgb, var(--accent-primary) 28%, transparent);
    }
    .wn-kofi-star {
        opacity: 0.75;
        transform: scale(1);
    }
}

/*
 * Slabs inside an overlay.
 *
 * An overlay is near-opaque because its text is what the reader acts on, so
 * what sits inside it cannot separate itself by being more see-through. It
 * separates the other way: its own surface step, the lit top edge, the shaded
 * bottom edge, and a small lift off the sheet it stands on.
 *
 * The cheat sheet's sections, the recents list and the tag cloud's field all
 * want the same thing, so they share one rule rather than three copies.
 *
 * Not on flat, like every other depth cue in the app.
 */
body:not([data-depth="flat"]) :is(
    .cheat-sheet-group,
    .recent-bookmarks-modal-list,
    .page-overview-modal-slab,
    .tag-cloud-modal-body,
    .wn-entries,
    .wn-earlier
) {
    background: var(--surface-2);
    border-radius: var(--radius-5, 8px);
    box-shadow:
        var(--edge-light),
        var(--edge-dark),
        0 2px 8px color-mix(in srgb, black 22%, transparent);
}

/* The section divider was doing the separating on its own; the slab does it
   now, and two lines where one will do reads as a seam. */
body:not([data-depth="flat"]) .cheat-sheet-group {
    border-bottom: none;
    /* The column owns the spacing between groups; a margin here doubles it. */
    margin-bottom: 0;
    padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
}

/*
 * What's New joins them.
 *
 * Its two lists are the same shape as the recents list -- rows inside one
 * block, not a block per row -- so the hairlines between entries stay and do
 * the separating within. The "earlier" section drew its own top border to
 * mark where it began; the slab says that now, and two lines where one will
 * do reads as a seam.
 */
body:not([data-depth="flat"]) :is(.wn-entries, .wn-earlier) {
    padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
}

body:not([data-depth="flat"]) .wn-earlier {
    border-top: none;
}

/*
 * The cheat sheet's header and foot, in the overlays spec's vocabulary.
 *
 * The panel named itself and then said "close" three times: a full-width Close
 * button, an ESC hint under it, and nothing at all in the header. The header
 * carries the name, the key that opens the sheet and the way out; the foot
 * says how much is in it.
 */
.modal.keyboard-cheat-sheet-modal .modal-header,
.modal.bookmark-qr-modal .modal-header {
    display: flex;
    align-items: center;
    gap: var(--space-3, 0.75rem);
    padding-bottom: var(--space-3, 0.75rem);
    border-bottom: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
}

.modal.keyboard-cheat-sheet-modal .modal-title,
.modal.bookmark-qr-modal .modal-title {
    text-transform: lowercase;
    font-size: var(--font-size-text);
    font-weight: var(--font-weight-semibold, 600);
}

.cheat-sheet-modal-key,
.bookmark-qr-modal-key {
    flex-shrink: 0;
    padding: 0.15rem 0.4rem;
    border-radius: var(--radius-3, 4px);
    background: color-mix(in srgb, var(--accent-primary) 14%, transparent);
    color: var(--accent-primary);
    font-size: var(--font-size-small, 0.72rem);
    font-weight: 700;
}

.cheat-sheet-modal-close,
.bookmark-qr-modal-close {
    margin-left: auto;
    flex-shrink: 0;
    border: none;
    background: transparent;
    color: var(--text-tertiary);
    font-family: inherit;
    font-size: var(--font-size-small, 0.72rem);
    opacity: 0.5;
    cursor: pointer;
}

.cheat-sheet-modal-close:hover,
.cheat-sheet-modal-close:focus-visible,
.bookmark-qr-modal-close:hover,
.bookmark-qr-modal-close:focus-visible {
    opacity: 1;
}

.modal.keyboard-cheat-sheet-modal .modal-actions,
#app-modal:has(.keyboard-cheat-sheet-modal) .modal-esc-hint,
.modal.bookmark-qr-modal .modal-actions,
#app-modal:has(.bookmark-qr-modal) .modal-esc-hint {
    display: none;
}

/*
 * Pinned to the foot of the scrolling sheet.
 *
 * One open section is taller than the panel, so a foot in the flow sat under
 * fifteen groups and was only reachable by scrolling past all of them -- which
 * is the opposite of what a line saying how to close the thing is for.
 */
.cheat-sheet-foot {
    position: sticky;
    bottom: 0;
    z-index: 1;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-3, 0.75rem);
    margin-top: var(--space-2, 0.5rem);
    padding-bottom: var(--space-1, 0.25rem);
    padding-top: var(--space-3, 0.75rem);
    /*
     * Opaque, or the rows scroll straight through the line covering them.
     * --background-modal carries alpha; the same rgb() trick the inline editor
     * uses takes it off without naming a second colour.
     */
    background: var(--background-primary);
    border-top: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
    color: var(--text-tertiary);
    font-size: var(--font-size-small, 0.65rem);
}

.cheat-sheet-foot > span {
    opacity: 0.6;
}

@supports (background: rgb(from red r g b)) {
    .cheat-sheet-foot {
        background: rgb(from var(--background-modal, var(--background-primary)) r g b / 1);
    }
}

.cheat-sheet-foot-key,
.bookmark-qr-foot-key {
    padding: 0.1rem 0.35rem;
    border-radius: 2px;
    background: color-mix(in srgb, var(--text-primary) 12%, transparent);
}

/*
 * What's New wears the same panel chrome as the other overlays.
 *
 * It named itself and then said "close" three times: a full-width button, an
 * ESC hint under it, and nothing at all in the header. The header carries the
 * name and the way out; the foot already holds the update line and the Ko-fi
 * link, so the Esc hint joins them there.
 */
.modal.whats-new-modal .modal-header {
    display: flex;
    align-items: center;
    gap: var(--space-3, 0.75rem);
    padding-bottom: var(--space-3, 0.75rem);
    border-bottom: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
}

.wn-modal-close {
    margin-left: auto;
    flex-shrink: 0;
    border: none;
    background: transparent;
    color: var(--text-tertiary);
    font-family: inherit;
    font-size: var(--font-size-small, 0.72rem);
    opacity: 0.5;
    cursor: pointer;
}

.wn-modal-close:hover,
.wn-modal-close:focus-visible {
    opacity: 1;
}

/*
 * The real What's New panel, not everything wearing its class.
 *
 * The analytics notice borrows `whats-new-modal` for the same shell
 * (analytics-notice.js passes 'whats-new-modal analytics-notice-modal'), so
 * hiding the action row by class took its *Turn on* button away with it --
 * a modal whose whole purpose is that button. `.wn-content` is the release
 * notes' own body, which only the panel this rule is about renders.
 */
.modal.whats-new-modal:has(.wn-content) .modal-actions,
#app-modal:has(.wn-content) .modal-esc-hint {
    display: none;
}

.wn-foot-esc {
    flex-shrink: 0;
    /* At the right whether or not the update line is showing. */
    margin-left: auto;
    color: var(--text-tertiary);
    font-size: var(--font-size-small, 0.65rem);
    opacity: 0.6;
    white-space: nowrap;
}

.wn-foot-key {
    padding: 0.1rem 0.35rem;
    border-radius: 2px;
    background: color-mix(in srgb, var(--text-primary) 12%, transparent);
}

/* ── The pages sheet ──────────────────────────────────────────────────────────
   The panel is opened from the page strip and answers the question the strip
   asks, so it drops out of the band the strip stands in rather than floating
   in the middle of the screen: the band's bottom edge and the sheet's top edge
   are one line, the sheet is centred on the strip, and only its bottom corners
   are round. Past a handful of pages it spans the page's own column and lays
   the rows out in a grid; under that it is as wide as what it holds.
   ────────────────────────────────────────────────────────────────────────── */

.modal-overlay:has(.header-sheet) {
    /* The sheet places itself against the band; the overlay stops centring it. */
    justify-content: flex-start;
    align-items: stretch;
}

.modal.header-sheet {
    position: fixed;
    top: var(--header-sheet-top, 96px);
    /* On the strip's own centre line, which is the header row's: published by
       showPageOverlay so the sheet follows the row at every width. */
    left: var(--header-sheet-mid, 50%);
    transform: translateX(-50%) translateY(-6px);
    margin: 0;
    /* Square where it meets the band, round where it leaves the page. */
    border-top-left-radius: 0;
    border-top-right-radius: 0;
    border-top: 0;
    max-height: min(70vh, calc(100vh - var(--header-sheet-top, 96px) - 2rem));
    padding: var(--space-3, 0.8rem) var(--space-4, 1.15rem) var(--space-3, 0.8rem);
}

/*
 * The sheet drops out of the band, and keeps its own centring while it does.
 *
 * The shared open animation animates `transform` from the control that was
 * pressed, which overwrites the translate that puts this panel on the strip's
 * centre line -- the sheet landed half its own width to the right. It gets an
 * animation of its own instead: the same short fall, with the centring carried
 * through every frame.
 */
.modal-overlay.show .modal.header-sheet,
.modal-overlay.show .modal.header-sheet.from-origin {
    animation: header-sheet-drop 0.16s cubic-bezier(0.16, 1, 0.3, 1) both;
    transform: translateX(-50%) translateY(0);
}

@keyframes header-sheet-drop {
    from {
        transform: translateX(-50%) translateY(-8px);
        opacity: 0;
    }
    to {
        transform: translateX(-50%) translateY(0);
        opacity: 1;
    }
}

body.no-animations .modal-overlay.show .modal.header-sheet,
body.no-animations .modal-overlay.show .modal.header-sheet.from-origin {
    animation: none;
}

/* theme.css clears every transform under no-animations; this one is the
   centring, not motion, so it comes back. */
body.no-animations .modal.header-sheet {
    transform: translateX(-50%) !important;
}

@media (prefers-reduced-motion: reduce) {
    .modal-overlay.show .modal.header-sheet,
    .modal-overlay.show .modal.header-sheet.from-origin {
        animation: none;
    }
}

/* Wide: the page's own column, the same width the grid below it has. */
.modal.header-sheet.is-grid {
    width: min(var(--header-sheet-width, 60rem), calc(100vw - 2.5rem));
    max-width: calc(100vw - 2.5rem);
}

/* Narrow: as wide as the rows it holds, with a floor so two short names do not
   draw a sheet the size of a tooltip. */
.modal.header-sheet.is-compact {
    width: max-content;
    min-width: min(26rem, calc(100vw - 2.5rem));
    max-width: min(52rem, calc(100vw - 2.5rem));
}

/*
 * The rows lay out across the sheet.
 *
 * One column of nine rows is a list you scroll to read; three columns of three
 * is a list you take in at once. The cells keep the row's own height, so what
 * changes is how they are arranged, not what a row is.
 */
/*
 * Three columns at most, fewer when the sheet is narrow.
 *
 * A minimum in rem lets a wide sheet fit four or five, and five names side by
 * side is a table again. The floor is a share of the sheet instead, so the
 * count falls out of the width: three across the page's column, two on a
 * half-width window, one on a phone.
 */
.modal.page-overview-sheet.is-grid .page-overview-modal-slab {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(30%, 1fr));
    gap: 0;
    /*
     * The rows scroll, and they are the only thing that does.
     *
     * In one column the <ul> was the scroller; here the list is the grid's
     * cells (display: contents) and has no box to scroll, so the slab holding
     * them takes the job -- otherwise the whole sheet grew a scrollbar and
     * took the filter and the foot out of view with it.
     */
    overflow-y: auto;
    min-height: 0;
}

/* The list is the grid's cells rather than a box inside one cell. */
.modal.page-overview-sheet.is-grid .page-overview-modal-list {
    display: contents;
}

/* The way to make a page is the last cell, not a row under the grid. */
.modal.page-overview-sheet.is-grid .page-overview-modal-actions {
    display: flex;
    align-items: center;
}

@media (max-width: 1100px) {
    .modal.page-overview-sheet.is-grid .page-overview-modal-slab {
        grid-template-columns: repeat(auto-fill, minmax(45%, 1fr));
    }
}

@media (max-width: 720px) {
    .modal.page-overview-sheet.is-grid .page-overview-modal-slab {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* The hairlines follow the grid: every cell carries the line under it, and the
   ones that are not in the first column carry the line beside it. */
.modal.page-overview-sheet.is-grid .page-overview-modal-item {
    border-right: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
}

.modal.page-overview-sheet.is-compact .page-overview-modal-list {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
}

.modal.page-overview-sheet.is-compact .page-overview-modal-item {
    flex: 1 1 14rem;
    border-right: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
}

/* The way to make a page is the last cell of the grid rather than a row under
   it: same height, same hairlines, a + where a page carries its number. */
.modal.page-overview-sheet .page-overview-modal-actions:not(.is-alone) {
    border-top: 0;
}

.modal.page-overview-sheet .page-overview-modal-foot {
    padding-left: 0;
    padding-right: 0;
}

/* ── What's new, the 2026-09 redesign ─────────────────────────────────────
   The modal glows the way the panels and list rows do: an accent ring and a
   soft accent bloom behind the version, and each entry the left-edge glow of
   --lvs-glow -- the accent for something new, a steady blue for a fix. Two
   colours are fixed rather than themed: Ko-fi's own coral, so the ask is
   recognised for what it is, and the fix blue, which has to stay apart from
   whatever the accent is. */
.modal.whats-new-modal {
    --wn-fix: #5b9cf5;
    --wn-kofi: #ff5e5b;
    --wn-kofi-ink: #fff;
    background-image:
        radial-gradient(28rem 12rem at 16% -6%, color-mix(in srgb, var(--accent-primary) 18%, transparent), transparent 62%),
        radial-gradient(22rem 14rem at 110% 110%, color-mix(in srgb, var(--wn-fix) 8%, transparent), transparent 60%);
    border-color: color-mix(in srgb, var(--accent-primary) 30%, var(--border-primary));
}

/* The ring, stated where the themes' own modal shadow cannot outrank it --
   their --edge-light rule sits on html[data-theme] body, and the glow is what
   this modal was missing beside every other panel. */
html body #app-modal .modal.whats-new-modal {
    box-shadow:
        0 1px 0 color-mix(in srgb, #fff 6%, transparent) inset,
        0 0 0 1px color-mix(in srgb, var(--accent-primary) 8%, transparent),
        0 18px 48px -10px color-mix(in srgb, var(--accent-primary) 30%, transparent),
        0 30px 80px rgba(0, 0, 0, 0.5);
}

.modal.whats-new-modal .wn-hero {
    border-bottom: 0;
    margin-bottom: 0;
    padding-bottom: var(--space-3, 0.8rem);
}

.wn-hero-line {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--space-1, 0.3rem) var(--space-3, 0.8rem);
}

.modal.whats-new-modal .wn-hero-version {
    font-size: calc(var(--font-size-text) * 2.5);
    font-weight: var(--font-weight-bold, 700);
    letter-spacing: -0.02em;
    background: linear-gradient(180deg, var(--text-primary), color-mix(in srgb, var(--accent-primary) 55%, var(--text-primary)));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    text-shadow: 0 0 1.75rem color-mix(in srgb, var(--accent-primary) 25%, transparent);
}

.modal.whats-new-modal .wn-hero-meta { margin: 0; }
.wn-hero-counts { margin: var(--space-2, 0.5rem) 0 0; font-size: var(--font-size-controls); color: var(--text-tertiary); }

.wn-section-select {
    margin-left: auto;
    align-self: center;
    padding: 0.35rem 0.7rem;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-4, 0.45rem);
    background: color-mix(in srgb, var(--background-primary) 60%, transparent);
    color: var(--text-primary);
    font: inherit;
    font-size: var(--font-size-controls);
    cursor: pointer;
}
.wn-section-select:focus-visible, .wn-support-btn:focus-visible, .wn-fix summary:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
}

/* The section a change belongs to, above its title in a card and in front of
   it on a fix row: the label the tabs used to carry. */
.wn-entry-section {
    display: block;
    font-size: calc(var(--font-size-controls) * 0.8);
    font-weight: var(--font-weight-semibold);
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--accent-primary);
}

/* New changes as cards, as many across as the width allows. Each keeps its
   explanation, folded at three lines, so lines stay short in a wide modal. */
.modal.whats-new-modal .wn-entries.wn-cards {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
    gap: var(--space-2-5, 0.7rem);
}
/* The cards are their own surfaces; the slab the lists sit on would frame
   them twice. */
body:not([data-depth="flat"]) .modal.whats-new-modal .wn-entries.wn-cards {
    padding: 0;
    background: none;
    box-shadow: none;
}
.modal.whats-new-modal .wn-cards .wn-entry {
    display: flex;
    flex-direction: column;
    gap: var(--space-1-5, 0.4rem);
}
.modal.whats-new-modal .wn-cards .wn-badge { align-self: flex-start; padding: 0 0.4rem; }
.modal.whats-new-modal .wn-cards .wn-entry-section { margin-bottom: var(--space-hairline, 0.15rem); }

/* Fixes as one line each, two columns when there is room. */
.modal.whats-new-modal .wn-entries.wn-fix-list {
    display: block;
    columns: 2 22rem;
    column-gap: var(--space-6, 2rem);
}
.modal.whats-new-modal .wn-entry.wn-entry--row {
    display: block;
    break-inside: avoid;
    padding: var(--space-1-5, 0.45rem) 0;
    border-radius: 0;
    background: none;
    box-shadow: none;
    border-bottom: 1px solid color-mix(in srgb, var(--border-primary) 55%, transparent);
}
.modal.whats-new-modal .wn-entry.wn-entry--row:last-child { border-bottom: 0; }
.wn-entry--row .wn-entry-section {
    display: inline;
    margin-right: var(--space-2, 0.5rem);
    color: var(--text-tertiary);
}
.wn-entry--row .wn-entry-title { font-weight: var(--font-weight-medium, 500); }
.wn-fix summary {
    list-style: none;
    cursor: pointer;
    padding-right: 1.1rem;
    position: relative;
}
.wn-fix summary::-webkit-details-marker { display: none; }
.wn-fix summary::after {
    content: '›';
    position: absolute;
    right: 0.2rem;
    top: 0;
    color: var(--text-tertiary);
    transition: transform 0.15s ease;
}
.wn-fix[open] summary::after { transform: rotate(90deg); }
.wn-fix summary:hover .wn-entry-title { color: var(--accent-primary); }
.wn-fix .wn-entry-body { margin-top: var(--space-1, 0.3rem); }

/* Each entry its own lit row, like a list row with a status. */
.modal.whats-new-modal .wn-entries { display: flex; flex-direction: column; gap: var(--space-2, 0.5rem); }
.modal.whats-new-modal .wn-entry {
    --wn-glow: var(--accent-primary);
    padding: var(--space-2-5, 0.7rem) var(--space-3, 0.75rem) var(--space-2-5, 0.7rem) var(--space-3-5, 0.9rem);
    border-radius: var(--radius-5, 0.6rem);
    background:
        linear-gradient(90deg,
            color-mix(in srgb, var(--wn-glow) 17%, transparent) 0,
            color-mix(in srgb, var(--wn-glow) 5%, transparent) 38%,
            transparent 72%),
        color-mix(in srgb, var(--background-primary) 55%, transparent);
    box-shadow:
        inset 3px 0 0 color-mix(in srgb, var(--wn-glow) 80%, transparent),
        inset 0 0 0 1px color-mix(in srgb, var(--text-primary) 4%, transparent);
}
.modal.whats-new-modal .wn-entry + .wn-entry { border-top: 0; }
.modal.whats-new-modal .wn-entry--fix { --wn-glow: var(--wn-fix); }
.modal.whats-new-modal .wn-entry--keys { --wn-glow: var(--accent-warning); }
.modal.whats-new-modal .wn-entry[hidden],
.modal.whats-new-modal .wn-group[hidden],
.modal.whats-new-modal .wn-block[hidden] { display: none; }

.modal.whats-new-modal .wn-badge {
    justify-self: stretch;
    text-align: center;
    margin-top: 0.15rem;
    font-weight: var(--font-weight-bold, 700);
}
.modal.whats-new-modal .wn-badge--new {
    color: var(--background-primary);
    background: var(--accent-primary);
    border-color: var(--accent-primary);
    box-shadow: 0 0 0.75rem color-mix(in srgb, var(--accent-primary) 45%, transparent);
}
.modal.whats-new-modal .wn-badge--fix {
    color: var(--wn-fix);
    background: color-mix(in srgb, var(--wn-fix) 13%, transparent);
    border-color: color-mix(in srgb, var(--wn-fix) 45%, transparent);
}
.modal.whats-new-modal .wn-group-title { border-bottom: 0; }
.modal.whats-new-modal .wn-group-title::after {
    content: '';
    flex: 1;
    height: 1px;
    align-self: center;
    background: var(--border-primary);
    order: 1;
}
.modal.whats-new-modal .wn-group-title .wn-group-count { order: 2; }

/* The ask, after the notes. */
.wn-support {
    position: relative;
    margin: var(--space-5, 1.5rem) 0 var(--space-2, 0.5rem);
    padding: var(--space-4, 1rem);
    border-radius: var(--radius-6, 0.75rem);
    overflow: hidden;
    background:
        radial-gradient(22rem 10rem at 100% 0%, color-mix(in srgb, var(--wn-kofi) 24%, transparent), transparent 70%),
        linear-gradient(90deg, color-mix(in srgb, var(--wn-kofi) 15%, transparent) 0, transparent 70%),
        color-mix(in srgb, var(--background-primary) 55%, transparent);
    box-shadow:
        inset 3px 0 0 var(--wn-kofi),
        inset 0 0 0 1px color-mix(in srgb, var(--wn-kofi) 30%, transparent),
        0 10px 30px -12px color-mix(in srgb, var(--wn-kofi) 55%, transparent);
}
.wn-support-top { display: flex; gap: var(--space-3, 0.75rem); align-items: flex-start; }
.wn-support-cup {
    flex: none;
    display: grid;
    place-items: center;
    width: 2.5rem;
    height: 2.5rem;
    border-radius: var(--radius-5, 0.6rem);
    font-size: 1.35rem;
    background: color-mix(in srgb, var(--wn-kofi) 14%, transparent);
    box-shadow: 0 0 1.1rem color-mix(in srgb, var(--wn-kofi) 40%, transparent);
}
.wn-support-title { margin: 0; font-size: var(--font-size-text); font-weight: var(--font-weight-semibold); color: var(--text-primary); }
.wn-support-text { margin: var(--space-1, 0.25rem) 0 0; font-size: calc(var(--font-size-text) * 0.93); line-height: 1.55; color: var(--text-secondary); max-width: 48ch; }
.wn-support-cta { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2, 0.5rem) var(--space-3, 0.75rem); margin-top: var(--space-3-5, 0.9rem); }
.wn-support-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--space-1-5, 0.4rem);
    padding: 0.55rem 1rem;
    border-radius: var(--radius-5, 0.55rem);
    background: var(--wn-kofi);
    color: var(--wn-kofi-ink);
    font-weight: var(--font-weight-bold, 700);
    text-decoration: none;
    box-shadow:
        0 6px 20px -6px color-mix(in srgb, var(--wn-kofi) 80%, transparent),
        inset 0 0 0 1px color-mix(in srgb, #fff 18%, transparent);
    transition: filter 0.15s ease, transform 0.15s ease;
}
.wn-support-btn:hover { filter: brightness(1.08); transform: translateY(-1px); }
.wn-support-btn::after {
    content: '';
    position: absolute;
    inset: -3px;
    border-radius: calc(var(--radius-5, 0.55rem) + 3px);
    border: 1px solid color-mix(in srgb, var(--wn-kofi) 50%, transparent);
    animation: wn-support-halo 2.8s ease-in-out infinite;
    pointer-events: none;
}
@keyframes wn-support-halo {
    0%, 100% { opacity: 0.2; transform: scale(1); }
    50% { opacity: 0.9; transform: scale(1.03); }
}
.wn-support-note { font-size: var(--font-size-controls); color: var(--text-tertiary); }

/* The footer button, filled in Ko-fi's colour so it is there even when the
   card has scrolled away. */
.whats-new-modal .wn-kofi-btn--solid {
    background: var(--wn-kofi);
    color: var(--wn-kofi-ink);
    border-color: transparent;
    font-weight: var(--font-weight-bold, 700);
    box-shadow: 0 0 1rem color-mix(in srgb, var(--wn-kofi) 45%, transparent);
}
.whats-new-modal .wn-kofi-btn--solid:hover { background: var(--wn-kofi); filter: brightness(1.08); }

/* The update line as a chip, its dot in the status colour. */
.wn-foot .wn-update-check-status {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1-5, 0.4rem);
    margin: 0;
    padding: 0.2rem 0.65rem;
    border: 1px solid var(--border-primary);
    border-radius: 999px;
    font-size: calc(var(--font-size-controls) * 0.95);
}
.wn-foot .wn-update-check-status::before {
    content: '';
    width: 0.45rem;
    height: 0.45rem;
    border-radius: 50%;
    background: currentColor;
    flex: none;
}
.wn-foot .wn-update-check-status--error { border-color: color-mix(in srgb, var(--accent-error, #d1493f) 40%, transparent); background: color-mix(in srgb, var(--accent-error, #d1493f) 8%, transparent); }
.wn-foot .wn-update-check-status--warn { border-color: color-mix(in srgb, var(--accent-primary) 45%, transparent); background: color-mix(in srgb, var(--accent-primary) 8%, transparent); }

@media (prefers-reduced-motion: reduce) {
    .wn-support-btn::after { animation: none; opacity: 0.5; }
    .wn-support-btn:hover { transform: none; }
}

/* The same Support button at the top right, beside Esc x, sized like the
   footer's. The gap leaves room for its sparkles. */
.wn-head-tools {
    display: flex;
    align-items: center;
    gap: var(--space-4, 1rem);
    margin-left: auto;
}
.whats-new-modal .wn-kofi-btn--head {
    gap: var(--space-1, 0.3rem);
    padding: var(--space-hairline, 0.18rem) var(--space-2, 0.5rem);
    border-radius: var(--radius-3, 0.3rem);
    font-size: calc(var(--font-size-controls) * 0.88);
}
.whats-new-modal .wn-kofi-btn--head .wn-kofi-icon { width: 13px; height: 13px; }

/* Support and Esc sit in the header's top-right corner: Esc's right edge on
   the same line as the footer's "Esc to close" below it. */
.wn-head-tools .wn-modal-close { padding-right: 0; }

/* The QR window (bookmark-qr.js), in the cheat sheet's chrome: the header,
   key chip, Esc x and foot share its rules above. The bookmark is named like a
   section of the sheet. The code keeps its own white field in every theme -- a
   camera reads black on white, not the theme's colours -- so only the frame
   around it follows the theme. Large enough to scan from a metre, never wider
   than a phone screen. */
.modal.bookmark-qr-modal {
    width: min(24rem, calc(100vw - 2rem));
    max-width: min(24rem, calc(100vw - 2rem));
}
/* No actions row under it, so no gap kept for one. */
.modal.bookmark-qr-modal .modal-body {
    margin-bottom: 0;
}
.bookmark-qr {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-3, 0.75rem);
}
.bookmark-qr-name {
    padding: var(--space-1, 0.25rem) 0;
    border-bottom: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
    font-size: var(--text-xs, 0.7rem);
    font-weight: var(--font-weight-bold);
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--text-tertiary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.bookmark-qr-frame {
    align-self: center;
    width: min(16rem, 70vw);
    aspect-ratio: 1;
    padding: var(--space-1, 0.25rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--layout-radius-xs, 6px);
    background: #fff;
}
.bookmark-qr-code {
    display: block;
    width: 100%;
    height: 100%;
}
.bookmark-qr-url {
    overflow-wrap: anywhere;
    text-align: center;
    color: var(--text-secondary);
    font-size: var(--font-size-small, 0.72rem);
    user-select: all;
}
.bookmark-qr-foot {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-3, 0.75rem);
    padding-top: var(--space-3, 0.75rem);
    border-top: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
    color: var(--text-tertiary);
    font-size: var(--font-size-small, 0.65rem);
}
.bookmark-qr-foot > span {
    opacity: 0.6;
}
.bookmark-qr-foot .modal-button {
    flex: none;
    width: auto;
    margin: 0;
}

/*
 * The public demo's lock: a chip beside a panel that cannot be changed there,
 * and the one popover that says why (shared/demo-lock.js).
 */
.demo-lock-chip {
    margin-left: var(--space-2, 0.5rem);
    padding: 1px 8px;
    border: 1px solid var(--accent-primary);
    border-radius: 999px;
    background: transparent;
    color: var(--accent-primary);
    font: inherit;
    font-size: 0.72em;
    font-weight: 500;
    letter-spacing: 0;
    text-transform: none;
    vertical-align: middle;
    cursor: help;
}

.demo-lock-popover {
    max-width: min(26rem, calc(100vw - 32px));
    margin: auto;
    padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
    background: var(--background-secondary);
    border: 1px solid var(--accent-primary);
    border-radius: var(--radius-5, 0.5rem);
    color: var(--text-primary);
    font-family: var(--font-family-main, inherit);
    box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
}

.demo-lock-popover::backdrop {
    background: transparent;
}

.demo-lock-popover-title {
    margin: 0 0 var(--space-1, 0.25rem);
    font-weight: 600;
}

.demo-lock-popover-text {
    margin: 0;
    color: var(--text-secondary);
    line-height: 1.5;
}

.config-demo-lock {
    margin: 0;
    padding: 0;
    border: 0;
    min-width: 0;
    opacity: 0.55;
}

/* The demo bar (shared/demo-lock.js): filled, above the header, not closable. */
.demo-bar {
    position: relative;
    z-index: 1001;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3, 0.75rem);
    padding: 6px max(16px, env(safe-area-inset-left));
    background: var(--accent-primary);
    color: var(--background-primary);
    font-family: var(--font-family-main, inherit);
    font-size: 0.82rem;
    line-height: 1.3;
}

.demo-bar-label {
    flex: none;
    padding: 1px 7px;
    border-radius: 3px;
    background: var(--background-primary);
    color: var(--accent-primary);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.demo-bar-text {
    flex: 1 1 0;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.demo-bar-install {
    flex: none;
    color: inherit;
    font-weight: 600;
    text-decoration: underline;
    text-underline-offset: 2px;
}

@media (max-width: 720px) {
    .demo-bar-shared,
    .demo-bar-install-name {
        display: none;
    }
}

/* Config → Unraid in the demo: the note pointing to Community Apps. */
.demo-unraid-note {
    margin: 0 0 var(--space-4, 1rem);
    padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
    border: 1px solid var(--accent-primary);
    border-left-width: 3px;
    border-radius: 0;
    background: var(--background-secondary);
    color: var(--text-primary);
}

.demo-unraid-note-title {
    margin: 0 0 var(--space-1, 0.25rem);
    font-weight: 600;
}

.demo-unraid-note-text {
    margin: 0 0 var(--space-2, 0.5rem);
    color: var(--text-secondary);
    line-height: 1.5;
}

.demo-unraid-note-link {
    color: var(--accent-primary);
    font-weight: 600;
}

/* ── What's new, spotlight and catch-up (2026-10) ──────────────────────────
   The release the modal leads with puts its highlight first, with a picture
   or a short recording and a button to where it lives. The other new changes
   are rows, the fixes one line each. Releases the reader missed open under
   it, counted in one line above, and a row of chips narrows all of them. */
.wn-since {
    margin: 0 0 var(--space-3, 0.75rem);
    padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
    border-radius: var(--radius-5, 0.6rem);
    font-size: var(--font-size-controls);
    color: var(--text-secondary);
    background: color-mix(in srgb, var(--accent-primary) 10%, transparent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-primary) 30%, transparent);
}
.wn-since strong { color: var(--accent-primary); }

.wn-filter {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1-5, 0.4rem);
    margin: 0 0 var(--space-3, 0.75rem);
}
.wn-chip {
    padding: 0.15rem 0.7rem;
    border: 1px solid var(--border-primary);
    border-radius: 999px;
    background: none;
    color: var(--text-secondary);
    font: inherit;
    font-size: calc(var(--font-size-controls) * 0.92);
    cursor: pointer;
}
.wn-chip:hover { color: var(--text-primary); }
.wn-chip[aria-pressed="true"] {
    color: var(--text-primary);
    border-color: var(--accent-primary);
    background: color-mix(in srgb, var(--accent-primary) 12%, transparent);
}
.wn-chip:focus-visible, .wn-go:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
}

.modal.whats-new-modal .wn-rel[hidden] { display: none; }
.wn-rel + .wn-rel,
.wn-rel + .wn-support {
    margin-top: var(--space-5, 1.5rem);
}
.wn-rel + .wn-rel {
    padding-top: var(--space-4, 1rem);
    border-top: 1px solid var(--border-primary);
}
.wn-rel-head {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--space-1, 0.3rem) var(--space-3, 0.75rem);
}
.wn-rel-version {
    margin: 0;
    font-size: calc(var(--font-size-text) * 1.45);
    font-weight: var(--font-weight-bold, 700);
    color: var(--accent-primary);
}
.wn-rel-date { font-size: var(--font-size-controls); color: var(--text-tertiary); }
.wn-rel-unread {
    font-size: calc(var(--font-size-controls) * 0.85);
    color: var(--accent-primary);
}
.wn-rel-unread::before { content: '● '; }
.wn-rel-lead { margin: var(--space-1, 0.3rem) 0 0; color: var(--text-secondary); max-width: 62ch; }

/* The spotlight: picture beside the text when there is room, above it when
   there is not. */
.modal.whats-new-modal .wn-entries.wn-spots { gap: var(--space-3, 0.75rem); }
body:not([data-depth="flat"]) .modal.whats-new-modal .wn-entries.wn-spots {
    padding: 0;
    background: none;
    box-shadow: none;
}
.modal.whats-new-modal .wn-entry.wn-spot {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-3, 0.75rem);
    padding: var(--space-3, 0.75rem) var(--space-3-5, 0.9rem);
}
.modal.whats-new-modal .wn-entry.wn-spot--media {
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
    align-items: center;
}
@media (max-width: 720px) {
    .modal.whats-new-modal .wn-entry.wn-spot--media { grid-template-columns: minmax(0, 1fr); }
}
.wn-media {
    margin: 0;
    border-radius: var(--radius-4, 0.45rem);
    overflow: hidden;
    background: var(--background-primary);
    box-shadow: inset 0 0 0 1px var(--border-primary);
    line-height: 0;
}
.wn-media-img,
.wn-media-video {
    display: block;
    width: 100%;
    height: auto;
}
.wn-spot-text { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-1-5, 0.4rem); min-width: 0; }
.modal.whats-new-modal .wn-spot-title {
    margin: 0;
    font-size: calc(var(--font-size-text) * 1.2);
    font-weight: var(--font-weight-bold, 700);
    line-height: 1.3;
    color: var(--text-primary);
}
.wn-rel:not(.wn-rel--headline) .wn-spot-title { font-size: calc(var(--font-size-text) * 1.05); }

.wn-go {
    flex: none;
    align-self: flex-start;
    margin-top: var(--space-1, 0.3rem);
    padding: 0.25rem 0.75rem;
    border: 1px solid color-mix(in srgb, var(--accent-primary) 55%, transparent);
    border-radius: var(--radius-4, 0.45rem);
    background: color-mix(in srgb, var(--accent-primary) 10%, transparent);
    color: var(--accent-primary);
    font: inherit;
    font-size: var(--font-size-controls);
    font-weight: var(--font-weight-semibold, 600);
    white-space: nowrap;
    cursor: pointer;
}
.wn-go:hover { background: color-mix(in srgb, var(--accent-primary) 20%, transparent); }

/* Every other new change, one row: section, title and folded detail, button. */
.modal.whats-new-modal .wn-entry.wn-entry--line {
    display: grid;
    grid-template-columns: 8.5rem minmax(0, 1fr) auto;
    gap: var(--space-1, 0.3rem) var(--space-3, 0.75rem);
    align-items: baseline;
}
.wn-entry--line .wn-entry-section { margin: 0; }
.wn-entry--line .wn-go { margin-top: 0; }
@media (max-width: 720px) {
    .modal.whats-new-modal .wn-entry.wn-entry--line { grid-template-columns: minmax(0, 1fr); }
}

/* The update line in the footer is a status, not an alarm: GitHub's rate
   limit or a failed check reads in the warning colour, not as a red bar
   under the notes. */
.wn-foot .wn-update-check-status--error {
    color: var(--accent-warning, #e0a040);
    border-color: color-mix(in srgb, var(--accent-warning, #e0a040) 40%, transparent);
    background: color-mix(in srgb, var(--accent-warning, #e0a040) 8%, transparent);
}

/* A chip hides rows with [hidden]; the row layouts above set display and
   would otherwise win over it. */
.modal.whats-new-modal .wn-entries .wn-entry[hidden],
.modal.whats-new-modal .wn-groups .wn-block[hidden],
.modal.whats-new-modal .wn-groups .wn-group[hidden] { display: none; }

/* ==== css/app-notification.css ==== */
/* Unified toast / notification host (#app-notification) */

.app-notification {
    position: fixed;
    right: max(1rem, env(safe-area-inset-right));
    bottom: max(1rem, env(safe-area-inset-bottom));
    max-width: min(380px, calc(100vw - 1.5rem));
    padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
    display: inline-flex;
    align-items: center;
    gap: var(--space-3, 0.75rem);
    font-size: var(--font-size-text);
    font-weight: var(--font-weight-semibold);
    font-family: var(--font-family-main);
    border: 1px solid var(--border-primary);
    background: var(--background-secondary);
    color: var(--text-primary);
    border-radius: var(--radius-4, 0.35rem);
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.14);
    opacity: 0;
    visibility: hidden;
    transform: translateY(12px);
    transition: opacity 0.24s ease, visibility 0.24s ease, transform 0.24s ease;
    z-index: 2001;
    pointer-events: none;
}

.app-notification.show {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
    pointer-events: auto;
}

.app-notification.has-action {
    max-width: min(420px, calc(100vw - 1.5rem));
}

.app-notification.success {
    color: var(--accent-success);
    border-color: var(--accent-success);
}

.app-notification.error {
    color: var(--accent-error);
    border-color: var(--accent-error);
}

.app-notification.warning {
    color: var(--accent-warning);
    border-color: var(--accent-warning);
}

.app-notification.info {
    color: var(--text-secondary);
    border-color: var(--border-primary);
}

/* Software update available — accent brand colour, distinct from success/error. */
.app-notification.update {
    color: var(--accent-primary);
    border-color: var(--accent-primary);
    background: color-mix(in srgb, var(--accent-primary) 10%, var(--background-secondary));
}

/* One-time discoverability promos — themed like config setting highlights. */
.app-notification.promo {
    color: var(--text-primary);
    border-color: color-mix(in srgb, var(--accent-primary) 42%, var(--border-primary));
    background: color-mix(in srgb, var(--accent-primary) 11%, var(--background-secondary));
    box-shadow:
        0 4px 18px color-mix(in srgb, var(--accent-primary) 16%, transparent),
        0 0 0 1px color-mix(in srgb, var(--accent-primary) 14%, transparent);
}

.app-notification.promo .app-notification-text {
    font-weight: var(--font-weight-medium);
}

.app-notification-promo-label {
    display: inline-block;
    margin-right: var(--space-1-5, 0.35rem);
    padding: var(--space-hairline, 0.1rem) var(--space-1-5, 0.42rem);
    border-radius: var(--radius-pill, 999px);
    font-size: 0.72em;
    font-weight: var(--font-weight-semibold);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    vertical-align: baseline;
    color: var(--accent-primary);
    background: color-mix(in srgb, var(--accent-primary) 18%, transparent);
}

/* Session tip: a longer, quieter toast that carries inline <code> keys. */
.app-notification.info .app-notification-text {
    font-weight: var(--font-weight-medium);
}

.app-notification-tip-label {
    color: var(--accent-warning);
    font-weight: var(--font-weight-semibold);
    text-transform: uppercase;
    font-size: 0.78em;
    letter-spacing: 0.06em;
    margin-right: var(--space-hairline, 0.15rem);
}

.app-notification-text code {
    font-family: var(--font-family-main);
    font-size: 0.88em;
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-2, 3px);
    padding: var(--space-hairline, 0.03rem) var(--space-1, 0.26rem);
    color: var(--text-primary);
    white-space: nowrap;
}

.app-notification-text {
    flex: 1;
    min-width: 0;
    line-height: 1.35;
}

.app-notification-action {
    flex-shrink: 0;
    border: 1px solid currentColor;
    background: transparent;
    color: inherit;
    border-radius: var(--radius-3, 0.25rem);
    padding: var(--space-1, 0.25rem) var(--space-2, 0.55rem);
    font-size: var(--font-size-controls);
    font-family: inherit;
    font-weight: var(--font-weight-semibold);
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.15s ease, border-color 0.15s ease;
}

.app-notification-action:hover {
    background: color-mix(in srgb, currentColor 12%, transparent);
}

.app-notification:not(.persist) {
    position: fixed;
}

.app-notification:not(.persist)::after {
    content: '';
    position: absolute;
    left: 0;
    bottom: 0;
    width: 100%;
    height: 2px;
    background: currentColor;
    opacity: 0.35;
    transform-origin: left center;
    transform: scaleX(0);
}

.app-notification.show:not(.persist)::after {
    animation: app-notification-timeout var(--notification-duration, 5s) linear forwards;
}

@keyframes app-notification-timeout {
    from { transform: scaleX(1); }
    to { transform: scaleX(0); }
}

@media (max-width: 575px) {
    .app-notification {
        right: max(0.75rem, env(safe-area-inset-right));
        bottom: max(0.75rem, env(safe-area-inset-bottom));
    }
}

body.no-animations .app-notification {
    transition: none !important;
    transform: none !important;
}

body.no-animations .app-notification.show:not(.persist)::after {
    animation: none !important;
}

@media (prefers-reduced-motion: reduce) {
    .app-notification { transform: none !important; transition: opacity 0.15s ease, visibility 0.15s ease !important; }
    .app-notification.show:not(.persist)::after { animation: none !important; }
}

/* ==== css/favicon-prefetch.css ==== */
/**
 * The blocking progress overlay.
 *
 * Written for the favicon batch prefetch and now shared with the source imports
 * on Config -> Data & backups -> Sources, which is why every rule carries a
 * second, service-neutral selector. One look for "nextDash is working through
 * something and you should wait", rather than a second panel that is almost the
 * same.
 *
 * Shared by config (after an import) and the dashboard (`:favicons fetch`), so
 * it lives outside the config-only stylesheets. Colours come from the theme
 * variables only.
 */
.favicon-prefetch-overlay,
.progress-overlay {
    position: fixed;
    inset: 0;
    z-index: 12000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-4, 1rem);
    background: color-mix(in srgb, var(--background-primary) 55%, transparent);
    backdrop-filter: blur(2px);
}

.favicon-prefetch-overlay[hidden],
.progress-overlay[hidden] {
    display: none !important;
}

.favicon-prefetch-panel,
.progress-overlay-panel {
    width: min(22rem, 100%);
    padding: var(--space-4, 1rem) var(--space-4, 1.1rem);
    border: 1px solid var(--border-primary);
    background: var(--background-secondary);
    box-shadow: 0 8px 28px rgba(0, 0, 0, 0.18);
    display: flex;
    flex-direction: column;
    gap: var(--space-3, 0.65rem);
}

.favicon-prefetch-title,
.progress-overlay-title {
    margin: 0;
    font-size: var(--font-size-controls);
    font-weight: var(--font-weight-semibold);
    color: var(--text-primary);
}

.favicon-prefetch-bar-track,
.progress-overlay-track {
    height: 0.45rem;
    border-radius: var(--radius-pill, 999px);
    overflow: hidden;
    background: color-mix(in srgb, var(--text-secondary) 18%, transparent);
}

.favicon-prefetch-bar-fill,
.progress-overlay-fill {
    height: 100%;
    width: 0;
    border-radius: inherit;
    background: var(--accent-primary);
    transition: width 0.2s ease;
}

.favicon-prefetch-status,
.progress-overlay-status {
    margin: 0;
    font-size: var(--font-size-small);
    color: var(--text-secondary);
}

/*
 * Step-by-step detail under the bar (ProgressOverlay.detail()): the panel
 * widens for it and the list scrolls inside, so a long log never pushes the
 * close button off screen.
 */
.progress-overlay-panel:has(.progress-overlay-detail:not(:empty)) {
    width: min(36rem, 100%);
}

.progress-overlay-detail {
    max-height: min(60vh, 32rem);
    overflow-y: auto;
    overscroll-behavior: contain;
}

.progress-overlay-detail:empty {
    display: none;
}

/*
 * The way out, for the work that has one.
 *
 * Quiet rather than prominent: it is not the expected answer, only the
 * available one, and a bright button beside a running bar invites the click it
 * exists to make possible rather than to encourage. Hidden entirely unless the
 * caller passed a way to stop.
 */
.progress-overlay-cancel {
    align-self: center;
    margin-top: var(--space-2, 0.5rem);
    padding: 0.25rem 0.9rem;
    border: 1px solid var(--border-primary, rgba(128, 128, 128, 0.4));
    border-radius: var(--layout-radius-sm, 8px);
    background: transparent;
    color: var(--text-secondary);
    font: inherit;
    font-size: var(--font-size-small);
    cursor: pointer;
}

.progress-overlay-cancel:hover:not(:disabled) {
    color: var(--text-primary);
    border-color: var(--text-secondary);
}

.progress-overlay-cancel:disabled {
    cursor: default;
    opacity: 0.6;
}

/*
 * The indeterminate bar.
 *
 * A source import is a single request: the server walks the service's API,
 * however many pages that takes, and answers once. There is no honest number to
 * put on that, and a bar sitting at zero until it jumps to a hundred reads as
 * frozen. So it sweeps instead -- which says "working", which is all that is
 * actually known.
 */
.progress-overlay-fill--indeterminate {
    width: 40%;
    transition: none;
    animation: progress-overlay-sweep 1.1s ease-in-out infinite;
}

@keyframes progress-overlay-sweep {
    0% { transform: translateX(-100%); }
    100% { transform: translateX(250%); }
}

/* Reduced motion gets a filled bar rather than a sweeping one. */
@media (prefers-reduced-motion: reduce) {
    .progress-overlay-fill--indeterminate {
        width: 100%;
        animation: none;
    }
}

/* ==== css/search.css ==== */
/* Search Component Styles */

/* Shortcut Search - anchored above bottom toolbar (classic + shared) */
.shortcut-search {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-end;
    padding: 0 max(1.25rem, 2vw) calc(var(--dashboard-toolbar-stack, 5.75rem) + env(safe-area-inset-bottom, 0px));
    box-sizing: border-box;
    z-index: 2000;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.2s ease-out, visibility 0s linear 0.2s;
    background: var(--modal-scrim);
    backdrop-filter: blur(var(--modal-scrim-blur));
    -webkit-backdrop-filter: blur(var(--modal-scrim-blur));
}

.shortcut-search.show {
    opacity: 1;
    visibility: visible;
    transition: opacity 0.2s ease-out, visibility 0s linear 0s;
}

/*
 * The command surface, following the overlay spec.
 *
 * No padding of its own: the prompt row, the results and the mode switch each
 * carry theirs, which is what lets the line under the prompt run the full
 * width of the panel instead of stopping short on both sides.
 *
 * The spec's 8px corner rather than 12: a panel that sits on the page and
 * holds a list of rows reads as a sheet, and a sheet has a tighter corner than
 * a card. Through the radius token, so a theme's own radius scale still
 * carries. Blur halved to 8px for the same reason the tiers elsewhere are
 * graded -- what is behind an overlay you read should stay out of the way
 * rather than churn.
 */
.search-container {
    width: min(52rem, calc(100vw - 2.5rem));
    max-width: 100%;
    border: 1px solid color-mix(in srgb, var(--border-primary) 75%, transparent);
    border-radius: var(--radius-5, 8px);
    background: color-mix(in srgb, var(--background-modal) 90%, transparent);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    box-shadow: 0 24px 64px color-mix(in srgb, black 60%, transparent);
    display: flex;
    flex-direction: column;
    max-height: min(70vh, calc(100vh - var(--dashboard-toolbar-stack, 5.75rem) - 2.5rem));
    overflow: hidden;
    box-sizing: border-box;
    /* exit: slide down toward toolbar */
    transform: translateY(10px) scale(0.98);
    opacity: 0;
    transition: transform 0.15s ease-in, opacity 0.15s ease-in;
}

/*
 * Out of the control that opened it, like every other panel.
 *
 * Same keyframe shape as the modals (modal.css): the class and `.show` arrive
 * together, so a transition would have no painted start state to run from.
 * Opened by key instead, the origin is unset and the ordinary rise below is
 * what plays.
 */
@keyframes search-from-origin {
    from {
        transform:
            translate(var(--modal-origin-x, 0), var(--modal-origin-y, 0))
            scale(0.4);
        opacity: 0;
    }
    to {
        transform: translateY(0) scale(1);
        opacity: 1;
    }
}

.shortcut-search.show .search-container.from-origin {
    animation: search-from-origin 0.16s cubic-bezier(0.16, 1, 0.3, 1) both;
}

body.no-animations .shortcut-search.show .search-container.from-origin {
    animation: none;
}

@media (prefers-reduced-motion: reduce) {
    .shortcut-search.show .search-container.from-origin {
        animation: none;
    }
}

.shortcut-search.show .search-container {
    transform: translateY(0) scale(1);
    opacity: 1;
    /* entrance: spring — vertraagt soepel aan het einde */
    transition: transform 0.38s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.22s ease-out;
}

/*
 * The prompt: a row of its own, ruled off from the results under it.
 *
 * It used to be a line with a margin under it inside a padded panel, so what
 * separated question from answer was white space. A rule the full width of
 * the sheet is what the spec draws, and it is what makes the panel read as
 * two parts rather than one column of things.
 */
.search-prompt {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    padding: var(--space-4, 1rem);
    border-bottom: 1px solid color-mix(in srgb, var(--border-primary) 45%, transparent);
    font-size: var(--font-size-bookmark);
    font-weight: var(--font-weight-semibold);
    justify-content: flex-start;
}

/*
 * The mode, as a pill: small, loud and tinted with the accent of the mode it
 * names. The gap on the row spaces it now, so it carries no margin of its own.
 */
.search-prefix {
    font-family: var(--font-family-main);
    color: var(--accent-success);
    font-size: 0.65rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 2px 6px;
    border-radius: var(--radius-3, 4px);
    background: color-mix(in srgb, var(--accent-success) 15%, transparent);
    border: 1px solid color-mix(in srgb, var(--accent-success) 35%, transparent);
    transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
    white-space: nowrap;
    flex-shrink: 0;
}

.search-prefix[data-mode="command"] {
    color: var(--accent-primary);
    background: color-mix(in srgb, var(--accent-primary) 15%, transparent);
    border-color: color-mix(in srgb, var(--accent-primary) 35%, transparent);
}

.search-prefix[data-mode="finder"] {
    color: var(--accent-secondary);
    background: color-mix(in srgb, var(--accent-secondary) 15%, transparent);
    border-color: color-mix(in srgb, var(--accent-secondary) 35%, transparent);
}

.search-prefix[data-mode="fuzzy"] {
    color: var(--text-secondary);
    background: color-mix(in srgb, var(--text-secondary) 12%, transparent);
    border-color: color-mix(in srgb, var(--text-secondary) 25%, transparent);
}

.search-prefix[data-mode="search"],
.search-prefix:not([data-mode]) {
    color: var(--accent-success);
    background: color-mix(in srgb, var(--accent-success) 15%, transparent);
    border-color: color-mix(in srgb, var(--accent-success) 35%, transparent);
}

.search-query {
    font-family: var(--font-family-main);
    color: var(--text-primary);
    white-space: nowrap;
    overflow-x: auto;
    display: inline-block;
    max-width: 100%;
    scrollbar-width: none; /* Firefox */
}

.search-query::-webkit-scrollbar {
    display: none; /* Chrome, Safari, and Opera */
}

.search-cursor {
    font-family: var(--font-family-main);
    animation: blink 1s infinite;
    margin-left: var(--space-1, 0.25rem);
    color: var(--text-primary);
}

@keyframes blink {
    0%, 50% { opacity: 1; }
    51%, 100% { opacity: 0; }
}

/* Mobile input - positioned off-screen but visible to Safari */
.search-input-mobile {
    position: absolute;
    left: -9999px;
    top: -9999px;
    width: 1px;
    height: 1px;
    border: none;
    padding: 0;
    margin: 0;
    opacity: 0.01;
}

.search-matches {
    display: flex;
    flex-direction: column;
    gap: var(--space-hairline, 0.15rem);
    /* The surface has none; the block that scrolls keeps its own. */
    padding: var(--space-2, 0.5rem) 0;
    flex: 1 1 auto;
    min-height: 0;
    max-height: none;
    overflow-y: auto;
    overflow-x: hidden;
    padding-left: 0;
    margin-left: 0;
    position: relative;
    width: 100%;
    scrollbar-width: none;
}

@media (max-width: 575px) {
    .search-container {
        width: calc(100vw - 1.5rem);
        max-height: calc(100dvh - 1.5rem);
        padding: var(--space-3, 0.85rem) var(--space-4, 1rem) var(--space-3, 0.65rem);
        border-radius: var(--radius-6, 10px);
    }

    .search-matches {
        padding-left: 0;
        margin-left: 0;
    }
}

.search-matches::-webkit-scrollbar {
    display: none; /* Chrome, Safari, and Opera */
}

.search-matches:empty {
    min-height: 0;
}

/*
 * A row runs the width of the sheet, and the 2px on its left edge is the only
 * thing that moves when it is the one you are on. It used to be a rounded chip
 * inset from both sides, which turned that edge into a floating tick.
 */
.search-match {
    padding: 0.45rem var(--space-4, 1rem);
    gap: 0.75rem;
    cursor: pointer;
    transition: background 0.1s ease, color 0.1s ease, border-color 0.1s ease;
    border: none;
    border-left: 2px solid transparent;
    border-radius: 0;
    background: transparent;
    text-align: left;
    display: flex;
    align-items: center;
    color: var(--text-primary);
    position: relative; /* Necesario para el ::before */
    /* Remove tap highlight on mobile */
    -webkit-tap-highlight-color: transparent;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

.search-match:hover,
.search-match.keyboard-selected {
    color: var(--text-primary);
    background: color-mix(in srgb, var(--accent-primary) 12%, transparent);
    border-left-color: var(--accent-primary);
    /* The light falls in from the edge that carries the bar, rather than
       sitting on the row as an even tint. */
    box-shadow: inset 4px 0 12px -4px color-mix(in srgb, var(--accent-primary) 24%, transparent);
    transform: none;
}

.search-match.keyboard-selected::before {
    content: none;
}

/* The setting's present value, among the choices a typed command lists —
   set on the row via match.current (see _markCurrentRow / _buildOnOffRows in
   search-commands.js). A background tint at rest, so it reads before you even
   reach for hover/arrow-keys; the second rule keeps hover's stronger tint on
   top rather than being flattened by this one on source-order alone. */
.search-match.is-current-value {
    background: color-mix(in srgb, var(--accent-primary) 6%, transparent);
}

.search-match.is-current-value:hover,
.search-match.is-current-value.keyboard-selected {
    background: color-mix(in srgb, var(--accent-primary) 9%, transparent);
}

.search-match-current-badge {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    margin-left: var(--space-1-5, 0.4rem);
    padding: var(--space-hairline, 0.1rem) var(--space-1-5, 0.4rem);
    border-radius: var(--radius-pill, 999px);
    border: 1px solid color-mix(in srgb, var(--accent-primary) 35%, transparent);
    background: color-mix(in srgb, var(--accent-primary) 18%, transparent);
    color: var(--accent-primary);
    font-size: var(--text-xs, 0.68rem);
    font-weight: var(--font-weight-semibold);
    text-transform: uppercase;
    letter-spacing: 0.02em;
}

/*
 * The same pill as the current-value badge, in the muted colour: it names where
 * the row came from rather than marking it as the chosen one, and a second
 * accent-coloured chip on the same line would compete with that one.
 */
.search-match-unsorted-badge {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    margin-left: var(--space-1-5, 0.4rem);
    padding: var(--space-hairline, 0.1rem) var(--space-1-5, 0.4rem);
    border-radius: var(--radius-pill, 999px);
    border: 1px solid var(--border-primary);
    background: var(--background-secondary);
    color: var(--text-secondary);
    font-size: var(--text-xs, 0.68rem);
    font-weight: var(--font-weight-semibold);
    text-transform: uppercase;
    letter-spacing: 0.02em;
}

.search-match.command-entry.command-just-applied {
    animation: command-palette-flash 0.55s ease;
}

@keyframes command-palette-flash {
    0%, 100% {
        background: color-mix(in srgb, var(--accent-primary) 9%, transparent);
    }
    45% {
        background: color-mix(in srgb, var(--accent-success) 28%, transparent);
        border-left-color: var(--accent-success);
    }
}

.search-match-shortcut {
    font-weight: var(--font-weight-bold);
    margin-right: var(--space-4, 1rem);
    min-width: 3ch;
    font-size: var(--font-size-controls);
    color: var(--accent-success);
}

.search-match-favicon-slot {
    width: 20px;
    height: 20px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-right: var(--space-2, 0.6rem);
    border-radius: var(--radius-4, 6px);
    overflow: hidden;
}

.search-match-favicon-slot .bookmark-icon {
    width: 16px;
    height: 16px;
    display: block;
}

.search-match-name {
    font-size: var(--font-size-text);
    font-weight: var(--font-weight-semibold);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    flex: 1;
    min-width: 0;
}

.search-match-meta {
    display: block;
    margin-top: var(--space-hairline, 0.08rem);
    font-size: var(--font-size-controls);
    font-weight: var(--font-weight-normal);
    color: var(--text-tertiary);
}

/* Command group headers */
.search-command-group-header {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    /* Indented to the row column, so a heading and the rows under it start on
       one line rather than two. */
    padding: var(--space-3, 0.75rem) var(--space-4, 1rem) var(--space-1, 0.25rem);
    cursor: pointer;
    user-select: none;
    margin-top: var(--space-1, 0.25rem);
    border-top: 1px solid color-mix(in srgb, var(--border-primary) 60%, transparent);
    opacity: 0.45;
    font-size: 0.65rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    transition: opacity 0.15s ease;
}

.search-command-group-header:first-child {
    border-top: none;
    margin-top: 0;
}

.search-command-group-header:hover,
.search-command-group-header.keyboard-selected {
    opacity: 1;
}

.search-command-group-header.keyboard-selected {
    color: var(--text-primary);
}

.search-command-group-header.keyboard-selected .search-command-group-label {
    color: var(--text-primary);
}

.search-command-group-arrow {
    font-size: 0.7em;
    width: 1em;
    text-align: center;
    color: var(--accent-success);
    flex-shrink: 0;
}

.search-command-group-label {
    font-size: var(--font-size-controls);
    font-weight: var(--font-weight-semibold);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-secondary);
    flex: 1;
}

/* Pushed to the far edge: the label says what the group is and the figure says
   how much of it there is, and the two belong at opposite ends of the line. */
.search-command-group-label {
    flex: 1;
    min-width: 0;
}

.search-command-group-count {
    font-size: var(--text-xs, 0.72rem);
    color: var(--text-tertiary);
    opacity: 0.7;
    flex-shrink: 0;
}

.search-match.command-group-child {
    padding-left: 1.25rem;
}

/* Special styling for config entry */
.search-match.config-entry .search-match-shortcut {
    color: var(--accent-warning);
}

.search-match.config-entry .search-match-name {
    color: var(--accent-warning);
}

.search-match.filter-completion-entry .search-match-shortcut {
    color: var(--accent-info);
}

.search-match.filter-completion-entry .search-match-name {
    color: var(--text-secondary);
}

.search-match.whats-new-entry .search-match-shortcut {
    color: var(--accent-warning, #f59e0b);
}

.search-match.whats-new-entry .search-match-name {
    color: var(--text-primary);
}

/* Finder entries */
.search-match.finder-entry .search-match-shortcut {
    color: var(--accent-info, #60a5fa);
}

/* No-match state */
.search-match.search-no-match-header {
    opacity: 0.9;
    cursor: default;
    pointer-events: none;
    padding: var(--space-2, 0.45rem) var(--space-3, 0.75rem);
}

.search-no-match-label {
    color: var(--text-secondary);
    margin-right: 0.3em;
}

.search-no-match-query {
    color: var(--text-primary);
    font-weight: var(--font-weight-semibold);
}

.search-match.search-no-match-header:hover,
.search-match.search-no-match-header.keyboard-selected {
    transform: none;
    color: inherit;
}

.search-match.search-hint-entry {
    opacity: 0.7;
    border-left: 2px solid transparent;
    padding-left: var(--space-1, 0.25rem);
    transition: opacity 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}

.search-match.search-hint-entry:hover {
    opacity: 1;
    border-left-color: var(--accent-primary, #4287f5);
}

.search-hint-shortcut {
    color: var(--accent-primary, #4287f5);
    font-family: "Source Code Pro", monospace;
    font-size: 0.82em;
    letter-spacing: 0.02em;
}

.search-hint-name {
    font-weight: var(--font-weight-normal);
    color: var(--text-secondary);
}

.search-match-use-count {
    flex-shrink: 0;
    font-size: var(--font-size-controls);
    color: var(--text-tertiary);
    font-weight: var(--font-weight-normal);
    min-width: 2ch;
    text-align: right;
    opacity: 0.7;
}

/*
 * With no buttons left in it, this is an anchor, not a row.
 *
 * The actions moved into the header and the container kept only the flow hint,
 * which is absolutely positioned against it. The plate below still drew: a
 * 24px blob of padding and blur at the bottom of the screen, a bubble over the
 * grid with nothing in it. The plate is for buttons, so it is drawn only when
 * there are any, and what is left takes no clicks.
 */
.button-container:not(:has(> *:not(.search-flow-hint))) {
    padding: 0;
    pointer-events: none;
}

/* Button Container - Centered at bottom */
/* Fixed max width so tip text length does not change container width (avoids button row jumping). */
.button-container {
    position: fixed;
    bottom: 2rem;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-content: flex-start;
    gap: var(--space-2, 0.55rem);
    z-index: 1000;
    width: min(52rem, calc(100vw - 2.5rem));
    max-width: 100%;
    box-sizing: border-box;
    padding: var(--space-3, 0.7rem) 0 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}

.search-flow-hint {
    /* Absolutely positioned above the button row — not in flex flow, so
       hiding it causes zero layout shift to the buttons below. */
    position: absolute;
    bottom: calc(100% + 0.85rem);
    left: 0;
    right: 0;

    display: flex;
    justify-content: center;
    align-items: center;

    font-size: var(--font-size-controls);
    color: var(--text-tertiary);
    letter-spacing: 0.05em;
    pointer-events: none;

    opacity: 1;
    transform: translateY(0);
    transition: opacity 0.45s ease, transform 0.45s ease;
}

.search-flow-hint[hidden] {
    display: none !important;
}

.search-flow-hint.dismissing {
    opacity: 0;
    transform: translateY(-8px);
}

.search-flow-chip,
kbd.search-flow-chip {
    display: inline-block;
    margin: 0 var(--space-hairline, 0.05rem);
    color: var(--accent-primary);
    font-family: var(--font-family-main);
    font-weight: var(--font-weight-semibold);
    text-shadow: 0 0 10px color-mix(in srgb, var(--accent-primary) 55%, transparent);
    border: none;
    background: transparent;
    padding: 0;
}

.search-flow-hint .sfh-seg-label {
    margin-left: 0.15em;
}

/* Each text segment wipes in left-to-right via clip-path — no extra chars, no fade */
.sfh-seg {
    display: inline-block;
    animation: sfhWipe 0.3s cubic-bezier(0.22, 1, 0.36, 1) both;
}

/* Chip chars get an extra scale-bounce on top of the wipe */
.sfh-seg.search-flow-chip,
.sfh-seg kbd.search-flow-chip {
    animation: sfhWipe 0.3s cubic-bezier(0.22, 1, 0.36, 1) both,
               sfhPop  0.38s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

/* Staggered delays: chips pause slightly longer after each separator */
.search-flow-hint-text > .sfh-seg:nth-child(1)  { animation-delay: 0.15s; }
.search-flow-hint-text > .sfh-seg:nth-child(2)  { animation-delay: 0.32s; }
.search-flow-hint-text > .sfh-seg:nth-child(3)  { animation-delay: 0.60s; }
.search-flow-hint-text > .sfh-seg:nth-child(4)  { animation-delay: 0.82s; }
.search-flow-hint-text > .sfh-seg:nth-child(5)  { animation-delay: 0.98s; }
.search-flow-hint-text > .sfh-seg:nth-child(6)  { animation-delay: 1.26s; }
.search-flow-hint-text > .sfh-seg:nth-child(7)  { animation-delay: 1.46s; }
.search-flow-hint-text > .sfh-seg:nth-child(8)  { animation-delay: 1.60s; }
.search-flow-hint-text > .sfh-seg:nth-child(9)  { animation-delay: 1.88s; }
.search-flow-hint-text > .sfh-seg:nth-child(10) { animation-delay: 2.08s; }
.search-flow-hint-text > .sfh-seg:nth-child(11) { animation-delay: 2.22s; }

@keyframes sfhWipe {
    from { clip-path: inset(0 100% 0 0); }
    to   { clip-path: inset(0 0%   0 0); }
}

@keyframes sfhPop {
    from { transform: scale(0.5); text-shadow: 0 0 24px color-mix(in srgb, var(--accent-primary) 100%, transparent); }
    65%  { transform: scale(1.3); }
    to   { transform: scale(1);   text-shadow: 0 0 10px color-mix(in srgb, var(--accent-primary) 55%, transparent); }
}

body.no-animations .search-flow-hint {
    transition: none !important;
}

body.no-animations .sfh-seg {
    animation: none !important;
    clip-path: none !important;
}

/* Custom tooltips on search buttons — appear above on hover */
.search-button[data-tooltip] {
    position: relative;
}

.search-button[data-tooltip]::after {
    content: attr(data-tooltip);
    position: absolute;
    bottom: calc(100% + 6px);
    left: 50%;
    transform: translateX(-50%);
    white-space: nowrap;
    padding: var(--space-1, 0.2rem) var(--space-2, 0.5rem);
    background: var(--background-secondary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-3, 0.3rem);
    color: var(--text-secondary);
    font-family: var(--font-family-main);
    font-size: calc(var(--font-size-controls) * 0.85);
    font-weight: var(--font-weight-normal);
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
    pointer-events: none;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.15s ease, visibility 0.15s ease;
    z-index: 1001;
}

.search-button[data-tooltip]:hover::after {
    opacity: 1;
    visibility: visible;
}

/* No tooltips on touch — hover doesn't exist there */
@media (pointer: coarse) {
    .search-button[data-tooltip]::after {
        display: none;
    }
}

body.no-animations .search-button[data-tooltip]::after {
    transition: none !important;
}

/* Search Button */
.search-button {
    /* Button styling */
    padding: var(--space-2, 0.62rem) var(--space-4, 0.92rem);
    border: none;
    /* --search-button-under is what the wash sits on: transparent by default,
       and the page background wherever the button floats over content with no
       plate beneath it. See the dock rule below. */
    background: color-mix(in srgb, var(--background-secondary) 74%, var(--search-button-under, transparent));
    box-shadow: var(--action-btn-shadow);
    color: var(--text-secondary);
    font-size: var(--font-size-controls);
    font-family: var(--font-family-main);
    font-weight: var(--font-weight-normal);
    cursor: pointer;

    /* TTY terminal style */
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    border-radius: var(--layout-radius-md, 0.65rem);

    /* Transitions */
    transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
    
    /* Remove tap highlight on mobile */
    -webkit-tap-highlight-color: transparent;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

.search-button:hover {
    background: var(--background-secondary);
    color: var(--text-primary);
    transform: none;
    box-shadow: var(--action-btn-shadow);
}

.search-button:active {
    transform: none;
    background: var(--background-secondary);
    color: var(--text-primary);
    box-shadow: none;
}

.search-button:focus-visible {
    outline: 2px solid var(--border-primary);
    outline-offset: 2px;
    color: var(--text-primary);
}

.search-button-icon {
    display: inline-block; /* required for transform to work on <span> */
    color: var(--text-secondary);
    font-weight: var(--font-weight-bold);
    font-size: var(--font-size-category);
}

.search-button:hover .search-button-icon,
.search-button:focus-visible .search-button-icon {
    color: var(--text-primary);
}

/* ── Periodic attention pulse on search / commands / finders icons ───────
   Total cycle = 10.8 s: 0.8 s of motion (0 – 7.4 %), then 10 s idle.
   The three icons are staggered 3.6 s apart so they pulse one by one.   */
@keyframes btnIconPulse {
    0%   { opacity: 1; }
    4%   { opacity: 0.45; }
    8%   { opacity: 1; }
    100% { opacity: 1; }
}

/*
 * Not in the header.
 *
 * The pulse was a hint for a glyph floating over the grid; in the header that
 * same span is the key printed on the button's corner, and a keycap that
 * blinks every ten seconds reads as a fault rather than an invitation.
 */
body:not(:has(.header-shortcuts)) :is(
    #search-button,
    .add-bookmark-button,
    .commands-button,
    .finders-button,
    .recent-button,
    .help-button
) .search-button-icon {
    animation: btnIconPulse 10.8s ease-in-out infinite;
}
#search-button .search-button-icon         { animation-delay: 1s;   }
.add-bookmark-button .search-button-icon  { animation-delay: 2.8s; }
.commands-button .search-button-icon       { animation-delay: 4.6s; }
.finders-button .search-button-icon        { animation-delay: 6.4s; }
.recent-button .search-button-icon         { animation-delay: 8.2s; }
.help-button .search-button-icon           { animation-delay: 9.4s; }

body.no-animations #search-button .search-button-icon,
body.no-animations .add-bookmark-button .search-button-icon,
body.no-animations .commands-button .search-button-icon,
body.no-animations .finders-button .search-button-icon,
body.no-animations .recent-button .search-button-icon,
body.no-animations .help-button .search-button-icon {
    animation: none !important;
}

/* Hide buttons when search is active */
.shortcut-search.show ~ .button-container {
    opacity: 0;
    pointer-events: none;
}

/* Preserve transforms when animations are disabled */
body.no-animations .search-button {
    transform: none !important;
}

/* Disable hover transforms when animations are disabled */
body.no-animations .search-button:hover {
    transform: none !important;
}

body.no-animations .search-button:active {
    transform: none !important;
}

/* Disable search match hover transforms when animations are disabled */
body.no-animations .search-match:hover,
body.no-animations .search-match.keyboard-selected {
    transform: none !important;
}

/* ─── Btn-group wrappers ──────────────────────────────────────────────────── */
/* In bottom-modus: transparant voor de bestaande flex-layout van .button-container */
.btn-group {
    display: contents;
}

/* Ensure button container remains centered when animations are disabled */
body.no-animations .button-container {
    transform: translateX(-50%) !important;
}

body.no-animations .shortcut-search,
body.no-animations .search-container {
    transition: none !important;
}

/* Fuzzy search highlight */
.fuzzy-highlight {
    font-weight: var(--font-weight-bold);
    text-transform: uppercase;
}

/* Query match highlight in search results */
mark.search-highlight {
    background: transparent;
    color: inherit;
    font-weight: var(--font-weight-bold);
    text-decoration: underline;
    text-decoration-color: var(--text-secondary);
    text-underline-offset: 2px;
}

/* Search mode tabs — shown at the bottom of the search container */
/*
 * One track, three positions.
 *
 * These are not three buttons to three places: search, commands and finders
 * are one panel that changes mode on a key, and this is how that is shown. A
 * shared border around the set says "pick one of these"; three separately
 * outlined pills said "here are three things".
 *
 * The modern layout has drawn it this way since it landed (overlays-modern.css
 * still refines the radii and transitions there). Classic is the default and
 * is where most readers are, so it gets the same treatment rather than a
 * looser one -- a switch should not look like a switch only for people who
 * changed a layout preference.
 */
.search-mode-tabs {
    display: flex;
    gap: var(--space-1-5, 0.4rem);
    padding: var(--space-1-5, 0.35rem);
    /* Centred under a rule of its own: the switch is the foot of the sheet,
       not the last row of the list. */
    margin: 0 auto var(--space-3, 0.75rem);
    border: 1px solid color-mix(in srgb, var(--border-primary) 60%, transparent);
    border-radius: var(--radius-6, 10px);
    background: color-mix(in srgb, var(--background-secondary) 55%, transparent);
    flex-shrink: 0;
    width: fit-content;
}

/* The plate the switch stands on, ruled off from the results above it. */
.search-mode-tabs-row {
    display: flex;
    justify-content: center;
    padding-top: var(--space-2, 0.5rem);
    border-top: 1px solid color-mix(in srgb, var(--border-primary) 45%, transparent);
    background: color-mix(in srgb, var(--background-primary) 60%, transparent);
    flex-shrink: 0;
}

/* The pill's own outline goes with it: inside a track, two borders around one
   thing read as a seam. The active position is told apart by its fill. */
.search-mode-tab {
    border-color: transparent;
}

.search-mode-tab {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1, 0.3rem);
    padding: var(--space-1, 0.2rem) var(--space-2, 0.55rem);
    border: 1px solid color-mix(in srgb, var(--border-primary) 70%, transparent);
    border-radius: var(--radius-pill, 999px);
    background: transparent;
    color: var(--text-tertiary);
    font-family: var(--font-family-main);
    font-size: calc(var(--font-size-controls) * 0.9);
    cursor: pointer;
    transition: background 0.13s ease, border-color 0.13s ease, color 0.13s ease;
    -webkit-tap-highlight-color: transparent;
    user-select: none;
    white-space: nowrap;
}

.search-mode-tab:hover {
    color: var(--text-secondary);
    border-color: var(--border-primary);
    background: color-mix(in srgb, var(--background-secondary) 60%, transparent);
}

.search-mode-tab:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
    border-radius: var(--radius-pill, 999px);
}

.search-mode-tab-key {
    font-weight: var(--font-weight-bold);
    font-size: 1em;
    line-height: 1;
}

/* Active states — match the search-prefix badge colours */
.search-mode-tab.active[data-mode="search"] {
    color: var(--accent-success);
    background: color-mix(in srgb, var(--accent-success) 15%, transparent);
    border-color: color-mix(in srgb, var(--accent-success) 35%, transparent);
    box-shadow: inset 0 0 15px color-mix(in srgb, var(--accent-success) 12%, transparent);
}

.search-mode-tab.active[data-mode="command"] {
    color: var(--accent-primary);
    background: color-mix(in srgb, var(--accent-primary) 15%, transparent);
    border-color: color-mix(in srgb, var(--accent-primary) 35%, transparent);
    box-shadow: inset 0 0 15px color-mix(in srgb, var(--accent-primary) 12%, transparent);
}

.search-mode-tab.active[data-mode="finder"] {
    color: var(--accent-secondary);
    background: color-mix(in srgb, var(--accent-secondary) 15%, transparent);
    border-color: color-mix(in srgb, var(--accent-secondary) 35%, transparent);
    box-shadow: inset 0 0 15px color-mix(in srgb, var(--accent-secondary) 12%, transparent);
}

/* Hide on mobile extra-small to save vertical space */
@media (max-width: 479px) {
    .search-mode-tabs {
        display: none;
    }
}

body.no-animations .search-mode-tab {
    transition: none !important;
}

/* ─── Recent search chips ────────────────────────────────────── */
.search-history-chip-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1-5, 0.35rem);
    padding: var(--space-1-5, 0.4rem) 1.25rem var(--space-2, 0.5rem);
}

.search-history-chip-wrap {
    display: inline-flex;
    align-items: center;
    gap: var(--space-hairline, 0.1rem);
    max-width: 100%;
}

.search-history-chip {
    display: inline-flex;
    align-items: center;
    padding: var(--space-1, 0.2rem) var(--space-2, 0.6rem);
    border-radius: var(--radius-pill, 999px);
    border: 1px solid var(--border-primary);
    background: transparent;
    color: var(--text-secondary);
    font-size: var(--text-xs, 0.72rem);
    font-family: inherit;
    line-height: 1.4;
    cursor: pointer;
    white-space: nowrap;
    max-width: 18ch;
    overflow: hidden;
    text-overflow: ellipsis;
    transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}

.search-history-chip:hover {
    background: color-mix(in srgb, var(--accent-primary) 10%, transparent);
    border-color: var(--text-secondary);
    color: var(--text-primary);
}

.search-history-chip:focus-visible,
.search-history-chip.keyboard-selected-chip {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
    border-radius: var(--radius-pill, 999px);
}

.search-history-chip.keyboard-selected-chip {
    background: color-mix(in srgb, var(--accent-primary) 14%, transparent);
    border-color: var(--accent-primary);
    color: var(--text-primary);
}

.search-history-chip-row.keyboard-selected {
    background: color-mix(in srgb, var(--accent-primary) 6%, transparent);
    border-radius: var(--radius-4, 0.35rem);
}

.search-history-chip-remove,
.search-history-remove {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.25rem;
    height: 1.25rem;
    padding: 0;
    border: none;
    border-radius: var(--radius-pill, 999px);
    background: transparent;
    color: var(--text-tertiary);
    font-size: var(--text-base, 0.95rem);
    line-height: 1;
    cursor: pointer;
    opacity: 0;
    transition: opacity 0.12s ease, color 0.12s ease, background 0.12s ease;
}

.search-history-chip-wrap:hover .search-history-chip-remove,
.search-match.history-entry:hover .search-history-remove,
.search-history-chip-remove:focus-visible,
.search-history-remove:focus-visible {
    opacity: 1;
    outline: 2px solid var(--accent-primary);
    outline-offset: 1px;
    border-radius: var(--radius-pill, 999px);
}

.search-history-chip-remove:hover,
.search-history-remove:hover {
    color: var(--accent-error);
    background: color-mix(in srgb, var(--accent-error) 12%, transparent);
}

.search-match.history-entry {
    display: flex;
    align-items: center;
    gap: var(--space-1-5, 0.35rem);
}

.search-match.history-entry .search-match-name {
    flex: 1 1 auto;
    min-width: 0;
}

body.no-animations .search-history-chip,
body.no-animations .search-history-chip-remove,
body.no-animations .search-history-remove {
    transition: none !important;
}

.search-command-chip {
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
    font-size: var(--text-2xs, 0.68rem);
    color: var(--accent-primary);
    border-color: color-mix(in srgb, var(--accent-primary) 35%, transparent);
}

.search-command-chip:hover {
    color: var(--accent-primary);
    border-color: var(--accent-primary);
}

@media (prefers-reduced-motion: reduce) {
    /* Spring entrance → opacity-only fade; remove scale/translate */
    .search-container {
        transform: none !important;
        transition: opacity 0.15s ease !important;
    }
    .shortcut-search.show .search-container {
        transition: opacity 0.2s ease !important;
    }
    /* Decorative blink cursor */
    .search-cursor { animation: none !important; opacity: 1 !important; }
    /* Staggered hint segments */
    .sfh-seg { animation: none !important; opacity: 1 !important; clip-path: none !important; }
    .search-flow-hint { transition: opacity 0.15s ease !important; }
}

/* The key that starts the mode, in front of the mode it starts. The tabs under
   the line name all four; this puts the convention where the typing happens, so
   "> to search" is legible without reading the legend — and so the way past a
   single-letter bookmark shortcut is visible when you need it. */
.search-chevron {
    font-family: var(--font-family-main);
    margin-right: var(--space-1, 0.3rem);
    font-size: 0.85em;
    font-weight: var(--font-weight-semibold);
    color: var(--accent-success);
    opacity: 0.85;
}

.search-chevron[data-mode="command"] { color: var(--accent-primary); }
.search-chevron[data-mode="finder"] { color: var(--accent-warning); }
.search-chevron[data-mode="global"],
.search-chevron[data-mode="fuzzy"] { color: var(--text-secondary); }

/* Shown only once there is something to clear. Escape does the same, but a line
   you typed into needs an exit you can point at — and touch has no Escape. */
.search-clear {
    margin-left: var(--space-2, 0.5rem);
    padding: 0 0.35em;
    border: 1px solid var(--border-secondary);
    border-radius: var(--layout-radius-sm, 8px);
    background: transparent;
    color: var(--text-secondary);
    font: inherit;
    font-size: 0.6em;
    line-height: 1.6;
    cursor: pointer;
}

.search-clear:hover,
.search-clear:focus-visible {
    color: var(--accent-error);
    border-color: color-mix(in srgb, var(--accent-error) 45%, transparent);
}

.search-clear[hidden] {
    display: none;
}

/*
 * The tail of a capped group.
 *
 * An open group stops at twelve rows so a common letter cannot fill the
 * overlay again. This row carries what is left, and reads as a way through
 * rather than as a result — quieter than a bookmark, louder than the header
 * above it, since it is the one row here you are meant to press.
 */
.search-group-more {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    padding: var(--space-1-5, 0.35rem) var(--space-2, 0.5rem);
    cursor: pointer;
    user-select: none;
    opacity: 0.6;
    font-size: var(--text-sm, 0.78rem);
    transition: opacity 0.15s ease;
}

.search-group-more:hover,
.search-group-more.keyboard-selected {
    opacity: 1;
}

.search-group-more.keyboard-selected {
    color: var(--text-primary);
    background: var(--bg-hover, color-mix(in srgb, var(--text-primary) 8%, transparent));
}

.search-group-more-arrow {
    opacity: 0.7;
}

/*
 * The bar as a surface of its own.
 *
 * It is the one free-floating control surface in the product: always there,
 * over whatever is on the page. Until now it was a transparent flex row and
 * every button carried its own plate, which read as a handful of loose
 * controls rather than as chrome.
 *
 * Airier than any other surface, deliberately -- the grid and the page wash
 * read through it -- and lifted by a deep shadow rather than by a glow. An
 * ambient halo was tried and rejected: a permanently glowing element competes
 * with the bloom that marks focus, and once two effects look alike neither
 * means anything.
 *
 * Not on flat. Whoever asked for flat asked for no plate either, and the
 * rules above that made this transparent stay the whole story there.
 */
body:not([data-depth="flat"]) .button-container:has(> *:not(.search-flow-hint)) {
    /* Sized to the buttons it holds, because a plate the width of the old
       transparent row would be a slab across half the screen. The ceiling is
       the one the row already had, so a narrow window still wraps inside it
       rather than overflowing. */
    width: fit-content;
    max-width: min(52rem, calc(100vw - 2.5rem));
    padding: var(--space-3, 0.7rem);
    border-radius: var(--radius-7, 12px);
    background: color-mix(in srgb, var(--background-secondary) 25%, transparent);
    backdrop-filter: blur(var(--glass-blur-page, 18px)) saturate(1.8);
    -webkit-backdrop-filter: blur(var(--glass-blur-page, 18px)) saturate(1.8);
    box-shadow:
        var(--edge-light),
        var(--edge-dark),
        0 12px 48px color-mix(in srgb, black 40%, transparent);
}

/* ── Recents: the last four as tiles, the rest as rows ────────────────────────
   Nine times in ten what you want from recents is one of the last few, and a
   row is a poor target for that -- one line of text among twenty identical
   lines. The four newest get a box with the time they were opened; everything
   older stays a row, and every row is one grid so a bookmark without a
   shortcut no longer shifts the name and the icon out of column.
   ────────────────────────────────────────────────────────────────────────── */

.search-recent-tiles {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--space-2, 0.5rem);
    padding: var(--space-3, 0.8rem) var(--space-4, 1rem);
    border-bottom: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
}

.search-recent-tile {
    display: grid;
    gap: var(--space-1, 0.3rem);
    padding: 0.55rem 0.6rem;
    border: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 55%, transparent));
    border-radius: var(--radius-5, 8px);
    background: color-mix(in srgb, var(--text-primary) 2%, transparent);
    cursor: pointer;
    min-width: 0;
    transition: background 0.1s ease, border-color 0.1s ease;
}

.search-recent-tile:hover,
.search-recent-tile.keyboard-selected {
    border-color: var(--accent-primary);
    background: color-mix(in srgb, var(--accent-primary) 10%, transparent);
}

.search-recent-tile-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2, 0.5rem);
}

/* The slot carries a row's right margin; in a tile it is one of two things on
   a line that sets its own spacing. */
.search-recent-tile-top .search-match-favicon-slot {
    margin-right: 0;
}

.search-recent-tile-top .search-match-shortcut {
    margin-right: 0;
    min-width: 0;
    font-size: calc(var(--font-size-controls) * 0.8);
}

.search-recent-tile-name {
    font-size: calc(var(--font-size-controls) * 0.95);
    color: var(--text-primary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.search-recent-tile-when {
    font-size: calc(var(--font-size-controls) * 0.8);
    color: var(--text-tertiary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/*
 * A row is a grid, not a line of things pushed along by whatever precedes them.
 *
 * As a flex row the shortcut was `min-width: 3ch` and absent on a bookmark
 * that has none, so the icon and the name of every such row started somewhere
 * else. One column for the key, whether or not there is one.
 */
.search-match.search-match--recent {
    display: grid;
    grid-template-columns: 4.5ch 20px minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-2, 0.6rem);
}

.search-match.search-match--recent .search-match-shortcut,
.search-match.search-match--recent .search-match-favicon-slot {
    margin-right: 0;
    min-width: 0;
}

/* The key column stands empty rather than closing up when there is no key. */
.search-match.search-match--recent .search-match-name {
    grid-column: 3;
    min-width: 0;
}

.search-match-when {
    color: var(--text-tertiary);
    font-size: calc(var(--font-size-controls) * 0.8);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* ── The scope rail, and the sections in the list ─────────────────────────────
   The panel answered one kind at a time, and which kind was set at the top and
   shown at the bottom -- a row of pills under the results, at the far end of
   the sheet from the line that sets it. The rail stands beside the list, names
   every kind with the key that scopes to it, and counts what the current query
   answers per kind. Tab walks it.
   ────────────────────────────────────────────────────────────────────────── */

.search-body {
    display: grid;
    grid-template-columns: 10.5rem minmax(0, 1fr);
    min-height: 0;
}

.search-scope-rail {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 1px;
    padding: var(--space-2, 0.45rem) 0;
    border-right: 1px solid color-mix(in srgb, var(--border-primary) 45%, transparent);
    min-width: 0;
}

/* In the rail a scope is a row, not a pill: the pill shape belonged to a
   centred track of three, and a column of six reads as a list. */
.search-scope.search-mode-tab {
    display: grid;
    grid-template-columns: 1.2rem minmax(0, 1fr) auto;
    gap: var(--space-2, 0.45rem);
    align-items: center;
    width: 100%;
    border: 0;
    border-radius: 0;
    padding: 0.3rem var(--space-3, 0.8rem);
    text-align: left;
    box-shadow: none;
    background: transparent;
}

.search-scope.search-mode-tab:hover {
    border: 0;
    background: color-mix(in srgb, var(--text-primary) 5%, transparent);
    color: var(--text-secondary);
}

/* The one it is in: a bar on the edge it is attached to, not an outline. */
.search-scope.search-mode-tab.active,
.search-scope.search-mode-tab.active[data-mode="search"],
.search-scope.search-mode-tab.active[data-mode="command"],
.search-scope.search-mode-tab.active[data-mode="finder"] {
    color: var(--text-primary);
    background: color-mix(in srgb, var(--accent-primary) 10%, transparent);
    border: 0;
    box-shadow: inset 2px 0 0 var(--accent-primary);
}

.search-scope .search-mode-tab-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.search-scope .search-mode-tab-key {
    color: var(--text-tertiary);
    font-size: calc(var(--font-size-controls) * 0.85);
}

.search-scope.active .search-mode-tab-key {
    color: var(--accent-primary);
}

.search-scope-count {
    color: var(--text-tertiary);
    font-size: calc(var(--font-size-controls) * 0.8);
    font-variant-numeric: tabular-nums;
}

/* A kind the query does not reach is dimmed rather than removed: a rail that
   changes length while you type is a rail you cannot aim at. */
.search-scope.is-empty {
    opacity: 0.35;
}

.search-scope-rule {
    height: 1px;
    margin: var(--space-2, 0.4rem) var(--space-3, 0.8rem);
    background: color-mix(in srgb, var(--border-primary) 45%, transparent);
}

.search-scope-hint {
    padding: var(--space-1, 0.2rem) var(--space-3, 0.8rem);
    color: var(--text-tertiary);
    font-size: calc(var(--font-size-controls) * 0.75);
    line-height: 1.35;
}

/* The heading over a kind's rows: what they are, and how many there are in all. */
.search-scope-section {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    padding: var(--space-2, 0.5rem) var(--space-4, 1rem) var(--space-1, 0.25rem);
    color: var(--text-tertiary);
    font-size: calc(var(--font-size-controls) * 0.75);
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

.search-scope-section::after {
    content: '';
    flex: 1;
    height: 1px;
    background: color-mix(in srgb, var(--border-primary) 35%, transparent);
    order: 1;
}

.search-scope-section-count {
    order: 2;
    letter-spacing: 0;
    text-transform: none;
    font-variant-numeric: tabular-nums;
}

/* What a capped section left out, and the key that shows all of it. */
.search-scope-more {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2, 0.5rem);
    padding: 0.3rem var(--space-4, 1rem) 0.5rem;
    color: var(--text-tertiary);
    font-size: calc(var(--font-size-controls) * 0.85);
    cursor: pointer;
    border-left: 2px solid transparent;
}

.search-scope-more:hover,
.search-scope-more.keyboard-selected {
    color: var(--text-secondary);
    background: color-mix(in srgb, var(--text-primary) 4%, transparent);
    border-left-color: var(--accent-primary);
}

.search-scope-more-key {
    font-weight: var(--font-weight-bold);
    color: var(--accent-primary);
}

/* Narrow: the rail lies down over the list as a row of pills. */
@media (max-width: 780px) {
    .search-body {
        grid-template-columns: minmax(0, 1fr);
    }

    .search-scope-rail {
        flex-direction: row;
        gap: var(--space-1, 0.3rem);
        overflow-x: auto;
        padding: var(--space-2, 0.45rem) var(--space-3, 0.7rem);
        border-right: 0;
        border-bottom: 1px solid color-mix(in srgb, var(--border-primary) 45%, transparent);
    }

    .search-scope.search-mode-tab {
        width: auto;
        grid-template-columns: auto auto auto;
        border: 1px solid color-mix(in srgb, var(--border-primary) 60%, transparent);
        border-radius: var(--radius-pill, 999px);
        padding: 0.1rem 0.6rem;
    }

    .search-scope.search-mode-tab.active {
        box-shadow: none;
        border: 1px solid var(--accent-primary);
    }

    .search-scope-rule,
    .search-scope-hint {
        display: none;
    }
}

/* Web search: the section under the list (search-web.js). */
.search-web { border-top: 1px solid var(--border-primary); margin-top: 6px; padding-top: 6px; }
/* Holds focus while results load (renderSearchMatches); a container, not a control. */
.search-web:focus { outline: none; }
.search-web-header { font-size: 0.75em; text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.7; padding: 4px 12px; }
.search-web-engine { margin-left: auto; font-size: 0.75em; opacity: 0.7; }
.search-web-result { align-items: flex-start; }
.search-web-letter {
    flex: 0 0 22px; height: 22px; border-radius: 6px; display: inline-flex; align-items: center; justify-content: center;
    font-size: 0.8em; font-weight: 600; background: var(--background-secondary); margin-right: 8px;
}
.search-web-text { display: flex; flex-direction: column; min-width: 0; }
.search-web-title { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.search-web-domain { font-size: 0.8em; opacity: 0.7; }
.search-web-snippet { font-size: 0.85em; opacity: 0.85; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.search-web-message { padding: 8px 12px; opacity: 0.8; }
.search-web-loading { display: flex; flex-direction: column; gap: 6px; padding: 6px 12px; }
.search-web-skeleton { display: block; height: 14px; border-radius: 4px; background: var(--background-secondary); opacity: 0.6; }
.search-web-badge {
    margin-left: auto; flex: 0 0 auto; font-size: 0.75em; padding: 2px 6px; border-radius: 999px;
    background: var(--background-secondary); white-space: nowrap; max-width: 40%; overflow: hidden; text-overflow: ellipsis;
}
.search-web-tabs { display: flex; gap: 4px; padding: 2px 12px 6px; overflow-x: auto; scrollbar-width: none; }
.search-web-tab {
    border: 1px solid var(--border-primary); background: transparent; color: inherit; border-radius: 999px;
    padding: 2px 10px; font-family: inherit; font-size: 0.8em; cursor: pointer; white-space: nowrap;
}
.search-web-tab[aria-selected="true"] { background: var(--background-secondary); font-weight: 600; }
.search-web-infobox { margin: 4px 12px 8px; padding: 8px 10px; border-radius: 8px; background: var(--background-secondary); }
.search-web-infobox summary { cursor: pointer; font-weight: 600; }
.search-web-infobox p { margin: 6px 0; font-size: 0.9em; }
/* The preview is a third column of the body (search-web.js showPreview): the
   panel clips anything hung outside its edge. */
.search-body:has(> .search-web-preview) { grid-template-columns: 10.5rem minmax(0, 1fr) minmax(0, 18rem); }
.search-web-preview {
    min-width: 0; min-height: 0; overflow: auto; padding: 14px;
    border-left: 1px solid color-mix(in srgb, var(--border-primary) 45%, transparent);
}
.search-web-preview-title { margin: 0 0 6px; font-size: 1em; }
.search-web-preview-url, .search-web-preview-date { margin: 0 0 6px; font-size: 0.8em; opacity: 0.7; word-break: break-all; }
@media (max-width: 780px) {
    .search-body:has(> .search-web-preview) { grid-template-columns: minmax(0, 1fr); }
    .search-web-preview { border-left: 0; border-top: 1px solid color-mix(in srgb, var(--border-primary) 45%, transparent); }
}
/* On a phone the entry row is the button: there is no Shift+Enter there. */
body[data-mobile-layout="true"] .search-web-entry { min-height: 48px; font-size: 1.05em; }

/* ==== css/bookmark-form-modal.css ==== */
/* Shared bookmark add/edit form — modal shell around the inline editor form. */

.bookmark-form-modal {
    display: none;
    align-items: center;
    justify-content: center;
    /* Breathing room on a tall window, less of it on a short one, where those
       pixels are the difference between the form fitting and scrolling. */
    padding: clamp(8px, 3vh, 24px) 16px;
    overflow-y: auto;
    z-index: 10050;
}

.bookmark-form-modal.show {
    display: flex;
}

/*
 * The cap is what the overlay actually leaves, not a flat 90vh.
 *
 * The overlay already holds 24px of padding top and bottom, so a 90vh dialog
 * asked for 90% of the height inside a box that had 48px taken off it — the
 * form fits in 735px, and on an 800px-tall window the cap came out at 720 and
 * put a scrollbar on a form with 15px to spare. Common laptop heights (800,
 * 810) landed just under the line, so the modal scrolled for no visible reason
 * while the same form sat still on a taller screen.
 *
 * 100dvh minus that padding lets the dialog use the room the overlay is really
 * offering, and dvh rather than vh so a mobile browser's collapsing toolbar
 * does not reintroduce the same few missing pixels. The subtraction mirrors the
 * overlay's clamped padding above — the two have to agree, or the cap starts
 * describing space the overlay is not giving.
 */
/*
 * The sheet, in the same language as the other overlays.
 *
 * This was the one overlay outside the depth ladder: its own corner, its own
 * shadow, no edges, and it never saw the glass step. The overlay spec draws
 * every panel the same way -- an 8px corner, a lit top edge and a shaded foot,
 * and a deep drop that says the thing is above the page rather than resting on
 * it. Through the tokens, so a theme's radius scale and depth choice reach it
 * like they reach everything else.
 *
 * The ground stays near-opaque. It is the overlay tier: you type into this,
 * and what is behind it has no business showing through.
 */
/*
 * Three grounds: the sheet, the groups on it, the fields cut into them.
 *
 * The form used to be painted with two: the sheet took the page colour and the
 * fields took the step above it, so every input sat *lighter* than the surface
 * holding it -- eleven raised boxes on a sheet that was the same colour as the
 * page behind it. The overlay spec has it the other way round, and so does
 * every physical control anyone has ever pressed: the panel lifts off the
 * page, and what you type into is cut back into the panel.
 *
 * These do not ride --theme-depth the way --surface-1 and --surface-2 do.
 * Depth decides how much a thing is lit and how far it casts, and at flat it
 * decides none of that -- which would collapse all three of these onto the
 * page colour and leave the form a single flat field of one colour. That a
 * sheet is a sheet is not a depth setting.
 *
 * On :root rather than on the open-modal class, because the form's surfaces
 * are resolved to opaque rgb through a probe element that is measured against
 * the body -- and that happens a frame before the class goes on, so scoped
 * here the probe read the fallback every time.
 */
:root {
    --bookmark-sheet-ground: color-mix(in srgb, var(--text-primary) 5%, var(--background-primary));
    --bookmark-sheet-slab: color-mix(in srgb, var(--text-primary) 9%, var(--background-primary));
    --bookmark-sheet-field: var(--background-primary);
}

.bookmark-form-modal-dialog {
    width: min(520px, 100%);
    max-height: min(calc(100dvh - (2 * clamp(8px, 3vh, 24px))), 920px);
    overflow: hidden;
    display: flex;
    flex-direction: column;
    margin: auto;
    flex-shrink: 0;
    border-radius: var(--radius-5, 8px);
    border: 1px solid color-mix(in srgb, var(--border-primary) 75%, transparent);
    /*
     * The colour every other modal in the app is painted in.
     *
     * It used to be --background-primary, which is the page. The sheet was
     * literally the same colour as what it covered, and the only thing saying
     * it was a sheet at all was the shadow under it -- so on a flat theme,
     * where there is no shadow, it had no edge of its own.
     */
    background: var(--bookmark-sheet-ground);
    box-shadow: 0 24px 64px color-mix(in srgb, black 60%, transparent);
}

/*
 * The edges and the drop, written where the depth ladder can reach them: one
 * declaration, because box-shadow does not stack across rules.
 */
html[data-theme] body:not([data-depth="flat"]) .bookmark-form-modal-dialog {
    box-shadow:
        var(--edge-light),
        var(--edge-dark),
        0 24px 64px color-mix(in srgb, black calc(60% * var(--theme-depth)), transparent);
}

.bookmark-form-modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 14px 16px 10px;
    border-bottom: 1px solid var(--border-primary, rgba(128, 128, 128, 0.15));
}

.bookmark-form-modal-title-group {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    min-width: 0;
}

.bookmark-form-modal-header h2 {
    margin: 0;
    font-size: calc(var(--font-size-text) * 1.12);
    font-weight: 600;
    /* The app lower-cases everywhere it names something. This was the exception. */
    text-transform: lowercase;
}

/* The key, as it is typed. Same chip the other overlays put beside their name. */
.bookmark-form-modal-key {
    flex-shrink: 0;
    padding: 0.15rem 0.4rem;
    border-radius: var(--radius-3, 4px);
    background: color-mix(in srgb, var(--accent-primary) 14%, transparent);
    color: var(--accent-primary);
    font-size: var(--font-size-small, 0.72rem);
    font-weight: 700;
    letter-spacing: 0.02em;
}

.bookmark-form-modal-close {
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    border: none;
    border-radius: var(--radius-5, 8px);
    background: transparent;
    color: inherit;
    font-size: 1.35rem;
    line-height: 1;
    cursor: pointer;
}

.bookmark-form-modal-close:hover {
    background: var(--background-secondary, rgba(128, 128, 128, 0.12));
}

.bookmark-form-modal-body {
    overflow-y: auto;
    padding: 14px 16px 18px;
}

.bookmark-form-modal-body .bookmark-inline-form {
    max-width: none;
    margin: 0;
    padding: 0;
    border: none;
    background: transparent;
    box-shadow: none;
}

/*
 * A phone gets the short form.
 *
 * Icon and Note are the two fields a phone is worst at and needs least: the
 * icon row is a preview plus four buttons for a favicon that is fetched from
 * the page anyway, and the note is a textarea on a keyboard that covers half
 * the screen. Between them they are about 200px of a form someone is filling in
 * with a thumb.
 *
 * `display: none`, not removed from the form: the inputs keep whatever the
 * bookmark already had, so editing one on a phone leaves its note and its icon
 * exactly as they were. Both are there in full on any desktop.
 */
@media (pointer: coarse) {
    .bookmark-form-modal-body [data-field-block="icon"],
    .bookmark-form-modal-body [data-field-block="note"] {
        display: none;
    }
}

/*
 * Two columns once the window can hold them.
 *
 * 860px is where two columns of about 410 still hold a select, the icon row and
 * the availability control without wrapping; the dialog itself is capped at 900
 * and simply uses what the window gives it below that. Under the breakpoint
 * nothing changes at all.
 *
 * `pointer: fine` keeps a phone or tablet on the single column whatever its
 * width says: a landscape phone is nearly 950px wide and is still a phone,
 * where one column you scroll beats two you pinch at. It reports the primary
 * pointer, so a desktop that happens to have a touchscreen still gets both
 * columns.
 *
 * Height is the point, not width: eleven full-width fields made the form 735px
 * tall for adding and 763px for editing, and a laptop browser with tabs open
 * leaves around 660px. Side by side the two groups come to roughly 350px, so
 * the dialog lands near 480px and the actions stay on screen.
 */
@media (min-width: 860px) and (pointer: fine) {
    /* One column now, so a narrower sheet: the fields are read top to
       bottom, and a 900px line of them is a long way for the eye. */
    .bookmark-form-modal-dialog {
        width: min(620px, 100%);
    }
}

/*
 * The buttons are the foot of the sheet, not the last field.
 *
 * They sat in the flow with a 4px margin, so what separated "fill this in"
 * from "do it" was the same gap that separates two fields. A rule the width of
 * the sheet is what every other overlay draws between a body and what closes
 * it.
 */
.bookmark-form-modal-body .bookmark-inline-actions {
    /*
     * Pinned to the foot of the sheet, the way every overlay's footer is.
     *
     * The buttons scrolled with the fields, so on a short window -- a laptop
     * with tabs open, a phone with the keyboard up -- Save was below the fold
     * of a form you had just finished filling in, and the way to reach it was
     * to scroll a form you were done with. Sticky rather than a real footer
     * element: the buttons belong to the form, and the form is shared with
     * whatever opens it.
     *
     * The negative margins pull the bar out to the body's own padding so the
     * rule above it spans the sheet instead of stopping short on both sides.
     */
    position: sticky;
    bottom: -18px;
    z-index: 1;
    margin: var(--space-3, 0.75rem) -16px -18px;
    padding: var(--space-3, 0.75rem) 16px 18px;
    background: var(--bookmark-sheet-ground);
    border-top: 1px solid color-mix(in srgb, var(--border-primary) 45%, transparent);
}

body.bookmark-form-modal-open .bookmark-inline-form,
body.bookmark-form-modal-open .bookmark-inline-form .bookmark-inline-input,
body.bookmark-form-modal-open .bookmark-inline-form .bookmark-inline-select,
body.bookmark-form-modal-open .bookmark-inline-form .bookmark-inline-textarea,
body.bookmark-form-modal-open .bookmark-inline-form .bookmark-inline-action-btn,
body.bookmark-form-modal-open .bookmark-inline-form .bookmark-inline-create-btn,
body.bookmark-form-modal-open .bookmark-inline-form .bookmark-inline-icon-preview {
    background: var(--inline-edit-field-bg, var(--background-secondary));
}

/* Delete/discard confirms and other #app-modal prompts opened from the form. */
body.bookmark-form-modal-open #app-modal.modal-overlay.show {
    /* A question the form asks is on top of everything the form has open:
       the tag dropdown (10150), a field bubble (10160), the page › category
       popover (10200). Below any of them it asks about a form it hides. */
    z-index: 10300;
}

/* Tag autocomplete dropdown (appended to body) must sit above the form modal. */
body.bookmark-form-modal-open .tag-ac-dropdown {
    z-index: 10150;
}

@media (max-width: 520px) {
    .bookmark-form-modal {
        padding: 12px 8px;
        align-items: center;
    }

    .bookmark-form-modal-dialog {
        max-height: none;
        min-height: min(90vh, 100%);
    }
}

/*
 * The two groups become two slabs.
 *
 * The columns have always been there and have always been silent: on a wide
 * window the gap between them was the only thing saying "these belong
 * together", and on a narrow one they ran into each other and the form was
 * back to the stack of rows the split was meant to break up. The overlay spec
 * draws a group as a slab -- a lifted panel with a lit top edge and a title
 * over a rule -- so that is what a group is here too.
 *
 * Both columns, at every width. `display: contents` below the breakpoint used
 * to dissolve them; it cannot any more, or the slab has nothing to paint. The
 * single column returns by the grid going away, not by the boxes going away.
 */
.bookmark-form-modal-body .bookmark-inline-col {
    display: flex;
    flex-direction: column;
    gap: var(--space-1-5, 0.35rem);
    min-width: 0;
    padding: var(--space-3, 0.75rem);
    border-radius: var(--radius-5, 8px);
    background: var(--bookmark-sheet-slab);
    box-shadow: var(--edge-light), var(--edge-dark), var(--surface-cast);
}

.bookmark-inline-group-title {
    margin: 0 0 var(--space-2, 0.5rem);
    padding-bottom: var(--space-1, 0.25rem);
    border-bottom: 1px solid var(--border-secondary, color-mix(in srgb, var(--border-primary) 50%, transparent));
    color: var(--text-tertiary);
    font-size: var(--font-size-small, 0.72rem);
    font-weight: 700;
    text-transform: lowercase;
    opacity: 0.75;
}

/*
 * A field is a hole in the slab, not another slab on top of it.
 *
 * The inputs were painted in --background-secondary while the sheet was
 * --background-primary, so every field sat a step *lighter* than the surface
 * holding it -- eleven raised boxes on a flat sheet, which is the arrangement
 * upside down. The sheet is the modal colour now and the slabs step up from
 * it, so the fields step back down to the page colour and read as recessed.
 *
 * Tighter corners than the slab, too. A 8px input inside an 8px group inside
 * an 8px sheet is three identical corners nesting; the spec's scale steps down
 * as the thing gets smaller.
 */
.bookmark-form-modal-body .bookmark-inline-input,
.bookmark-form-modal-body .bookmark-inline-select,
.bookmark-form-modal-body .bookmark-inline-textarea {
    border-radius: var(--radius-3, 4px);
}

/*
 * Focus, in the theme's own glow.
 *
 * A flat 2px halo in the accent colour was the one focus ring in the app that
 * did not know what depth the theme was set to. The bloom is the same light
 * every other lifted thing is lit with, so a flat theme gets a plain border
 * and a rich one gets the glow, without this rule having to know which.
 */
.bookmark-form-modal-body .bookmark-inline-input:focus,
.bookmark-form-modal-body .bookmark-inline-select:focus,
.bookmark-form-modal-body .bookmark-inline-textarea:focus {
    border-color: var(--accent-primary);
    box-shadow:
        0 0 0 1px color-mix(in srgb, var(--accent-primary) 45%, transparent),
        0 0 12px color-mix(in srgb, var(--accent-primary) calc(30% * var(--theme-depth, 1)), transparent);
}

/* The gap between the two groups when they are stacked. */
.bookmark-form-modal-body .bookmark-inline-form {
    gap: var(--space-3, 0.75rem);
}

/*
 * A field label is a caption, not a heading.
 *
 * They were set at the body size in title case, so "Icon URL (opt)" carried
 * the same weight as the group it sits in and rather more than the value
 * underneath it -- eleven headings down a form whose actual headings are the
 * two group titles. Smaller and lower-case, the way the app names everything
 * else it names.
 */
.bookmark-form-modal-body .bookmark-inline-label {
    font-size: var(--font-size-small, 0.78rem);
    font-weight: var(--font-weight-medium, 500);
    text-transform: lowercase;
    opacity: 0.85;
}

/*
 * The button that does the thing looks like it.
 *
 * Save, Create + New and Cancel were three outlined buttons in a row, so the
 * one that commits the form was told apart from the one that throws it away by
 * reading them. Filled while it can be pressed; when the form is not valid
 * yet it keeps the outline, so the fill arriving is also the form saying it
 * has what it needs.
 *
 * Scoped to the actions bar: `bookmark-inline-save` is not the save button's
 * name, it is a style hook, and the Set URL button in the icon row wears it
 * too. Filling that one as well put two identical green buttons in the form,
 * neither of which was then the one that commits it.
 */
.bookmark-form-modal-body .bookmark-inline-actions .bookmark-inline-save:not([aria-disabled="true"]):not(.bookmark-inline-save--invalid) {
    background: var(--accent-primary);
    border-color: var(--accent-primary);
    color: var(--background-primary);
    font-weight: var(--font-weight-semibold, 600);
}

/* One column, in the order the form is filled in. */
.bookmark-form-modal-body .bookmark-form-col {
    display: grid;
    gap: var(--space-3, 0.75rem);
}
.bookmark-form-modal-body .bookmark-form-place {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    gap: var(--space-3, 0.75rem);
}
.bookmark-form-modal-body .bookmark-form-flags-row {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    flex-wrap: wrap;
}
.bookmark-form-modal-body .bookmark-form-flags-row .bookmark-inline-toggles {
    margin-left: auto;
}
.bookmark-form-modal-body .bookmark-form-name-note {
    margin: 0.25rem 0 0;
    font-size: var(--text-2xs, 0.7rem);
    color: var(--text-tertiary);
}
.bookmark-form-modal-body .bookmark-inline-actions {
    justify-content: space-between;
}
.bookmark-form-modal-body .bookmark-inline-actions-end {
    display: flex;
    gap: var(--space-1-5, 0.35rem);
    margin-left: auto;
}
@media (max-width: 640px) {
    .bookmark-form-modal-body .bookmark-form-place { grid-template-columns: minmax(0, 1fr); }
    .bookmark-form-modal-body .bookmark-form-card { display: none; }
}

/* What the page says about itself: a quiet card, not a panel of controls. */
.bookmark-form-modal-body .bookmark-form-card {
    display: grid;
    grid-template-columns: 44px minmax(0, 1fr) auto;
    gap: var(--space-3, 0.85rem);
    align-items: center;
    padding: var(--space-2, 0.6rem);
    border: 1px solid var(--border-secondary);
    border-radius: var(--layout-radius-sm, 8px);
    background: var(--background-secondary);
}
.bookmark-form-modal-body .bookmark-form-card-icon {
    position: relative;
    width: 40px;
    height: 40px;
    border-radius: var(--radius-2, 6px);
    background: var(--background-primary);
    display: flex;
    align-items: center;
    justify-content: center;
}
.bookmark-form-modal-body .bookmark-form-card-icon img { width: 28px; height: 28px; object-fit: contain; }
.bookmark-form-modal-body .bookmark-form-card-pencil {
    position: absolute;
    right: -6px;
    bottom: -6px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    border: 1px solid var(--border-primary);
    background: var(--background-primary);
    color: var(--text-secondary);
    font-size: 11px;
    line-height: 1;
    cursor: pointer;
}
.bookmark-form-modal-body .bookmark-form-card-menu {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    z-index: 5;
    display: grid;
    min-width: 9rem;
    padding: 4px 0;
    border: 1px solid var(--border-primary);
    border-radius: var(--layout-radius-sm, 8px);
    background: var(--background-modal, var(--background-primary));
}
.bookmark-form-modal-body .bookmark-form-card-menu button {
    text-align: left;
    padding: 6px 10px;
    background: none;
    border: 0;
    color: var(--text-primary);
    font: inherit;
    cursor: pointer;
}
.bookmark-form-modal-body .bookmark-form-card-host { font-size: var(--text-xs, 0.75rem); color: var(--text-secondary); }
.bookmark-form-modal-body .bookmark-form-card-desc {
    margin: 2px 0 0;
    font-size: var(--text-2xs, 0.7rem);
    color: var(--text-tertiary);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.bookmark-form-modal-body .bookmark-form-card-image {
    width: 96px;
    height: 54px;
    object-fit: cover;
    border-radius: var(--radius-2, 6px);
}

.bookmark-form-modal-body .bookmark-form-name-action {
    padding: 0;
    border: 0;
    background: none;
    color: var(--text-secondary);
    font: inherit;
    text-decoration: underline;
    cursor: pointer;
}

/* Suggested tags under the tags field: quiet until you want them. */
.bookmark-form-modal-body .bookmark-form-tags-suggest {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
    margin-top: 6px;
}
.bookmark-form-modal-body .bookmark-form-tags-suggest[hidden] { display: none; }
.bookmark-form-modal-body .tag-suggest-chips-label {
    font-size: var(--text-2xs, 0.7rem);
    color: var(--text-tertiary);
}
.bookmark-form-modal-body .bookmark-form-tags-suggest-refresh {
    position: absolute;
    right: 8px;
    top: 26px;
    border: 0;
    background: none;
    color: var(--text-tertiary);
    cursor: pointer;
    font: inherit;
}
.bookmark-form-modal-body .bookmark-inline-field:has(> .bookmark-form-tags-suggest-refresh) { position: relative; }

/* Page › category: a field that opens the Move to… popover. */
.bookmark-form-modal-body .bookmark-form-place-value {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    width: 100%;
    text-align: left;
    cursor: pointer;
}
.bookmark-form-modal-body .bookmark-form-place-text {
    flex: 1 1 auto;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.bookmark-form-modal-body .bookmark-form-place-caret {
    color: var(--text-tertiary);
    font-size: 0.8em;
}
/* Above the bookmark sheet, which sits above the dashboard's popovers. */
.move-popover.bookmark-form-place-pop {
    z-index: 10200;
    max-width: min(24rem, calc(100vw - 1rem));
}
.bookmark-form-place-pop .bookmark-form-place-create .move-popover-check {
    color: var(--accent-primary);
}

/* `hidden` means hidden in this form. Several parts are kept in the DOM as
   the value the save reads -- the icon block, the page and category
   selects -- and a class that sets `display` would otherwise put them back. */
.bookmark-form-modal-body .bookmark-inline-form [hidden] {
    display: none !important;
}


/* Suggested tags look like the tags on a bookmark in Shift+T's popover --
   the same pill -- with a dashed edge because they are not on it yet. */
.bookmark-form-modal-body .bookmark-form-tags-suggest .tag-suggest-chip {
    background: var(--background-secondary);
    border: 1px dashed var(--border-secondary);
}
.bookmark-form-modal-body .bookmark-form-tags-suggest .tag-suggest-chip-add {
    font-size: calc(var(--font-size-controls) * 0.78);
    color: var(--text-tertiary);
    padding: var(--space-hairline, 0.12rem) 0.1rem var(--space-hairline, 0.12rem) var(--space-2, 0.45rem);
}
.bookmark-form-modal-body .bookmark-form-tags-suggest .tag-suggest-chip-add:hover,
.bookmark-form-modal-body .bookmark-form-tags-suggest .tag-suggest-chip-add:focus-visible {
    color: var(--text-primary);
}
.bookmark-form-modal-body .bookmark-form-tags-suggest .tag-suggest-chips-label {
    font-size: calc(var(--font-size-controls) * 0.72);
    font-weight: var(--font-weight-medium);
    color: var(--text-secondary);
}

/* The form's buttons are the theme's type, like every other control in it;
   left alone a <button> falls back to the browser's own face. */
.bookmark-form-modal-body .bookmark-inline-action-btn,
.bookmark-form-modal-body .bookmark-inline-create-btn,
.bookmark-form-modal-body .bookmark-form-card-pencil,
.bookmark-form-modal-body .bookmark-form-card-menu button,
.bookmark-form-modal-body .bookmark-form-place-value,
.bookmark-form-modal-body .bookmark-form-info {
    font-family: inherit;
}
.bookmark-form-modal-body .bookmark-form-card-letter {
    font-size: 1.1rem;
}
.move-popover.bookmark-form-place-pop .bookmark-form-place-filter {
    display: block;
    width: calc(100% - 2 * var(--space-3, 0.85rem));
    margin: 0 var(--space-3, 0.85rem) var(--space-1-5, 0.35rem);
    padding: var(--space-1, 0.3rem) var(--space-2, 0.5rem);
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-4, 0.4rem);
    background: var(--background-primary);
    color: var(--text-primary);
    font: inherit;
    font-size: calc(var(--font-size-controls) * 0.88);
}
.move-popover.bookmark-form-place-pop .bookmark-form-place-filter:focus {
    outline: none;
    border-color: var(--accent-primary);
}

/* A scroll that reaches the end of the form, or of its popover, stops there
   rather than carrying on into the page behind. */
.bookmark-form-modal-body,
.move-popover.bookmark-form-place-pop {
    overscroll-behavior: contain;
}
.bookmark-form-modal-body .bookmark-form-card.is-loading .bookmark-form-card-desc {
    font-style: italic;
    color: var(--text-tertiary);
}

/* The pencil: a real control on the icon's corner, not a speck. */
.bookmark-form-modal-body .bookmark-form-card-pencil {
    right: -8px;
    bottom: -8px;
    width: 24px;
    height: 24px;
    border: 1px solid color-mix(in srgb, var(--accent-primary) 55%, var(--border-primary));
    background: var(--background-secondary);
    color: var(--text-primary);
    font-size: 12px;
    box-shadow: 0 1px 4px color-mix(in srgb, black 35%, transparent);
    transition: border-color 0.12s ease, color 0.12s ease;
}
.bookmark-form-modal-body .bookmark-form-card-pencil:hover,
.bookmark-form-modal-body .bookmark-form-card-pencil:focus-visible {
    border-color: var(--accent-primary);
    color: var(--accent-primary);
    outline: none;
}

/* Before an address: a link mark in a dashed box, and one line saying what
   will appear. After a read that gave nothing: the same quiet line style. */
.bookmark-form-modal-body .bookmark-form-card-blank {
    display: inline-flex;
    color: var(--text-tertiary);
}
.bookmark-form-modal-body .bookmark-form-card-blank svg {
    width: 20px;
    height: 20px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.bookmark-form-modal-body .bookmark-form-card.is-idle .bookmark-form-card-icon {
    background: transparent;
    border: 1px dashed var(--border-primary);
}
.bookmark-form-modal-body .bookmark-form-card.is-idle .bookmark-form-card-host {
    display: none;
}
.bookmark-form-modal-body .bookmark-form-card.is-idle .bookmark-form-card-desc,
.bookmark-form-modal-body .bookmark-form-card.is-empty .bookmark-form-card-desc {
    color: var(--text-tertiary);
    font-style: italic;
    -webkit-line-clamp: 3;
}
.bookmark-form-modal-body .bookmark-form-card.is-empty .bookmark-form-card-image {
    display: none;
}
.bookmark-form-modal-body .bookmark-form-card-retry {
    margin-top: 4px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--text-secondary);
    font: inherit;
    font-size: var(--text-2xs, 0.7rem);
    text-decoration: underline;
    cursor: pointer;
}
.bookmark-form-modal-body .bookmark-form-card-retry:hover,
.bookmark-form-modal-body .bookmark-form-card-retry:focus-visible {
    color: var(--accent-primary);
}

/* The ⓘ after a label: the check group's own button, set beside the text. */
#bookmark-form-modal .bookmark-inline-label .bookmark-form-info {
    margin-left: 6px;
    vertical-align: 1px;
    cursor: help;
}

/* ── App icons from the icon sets (icon-set-picker.js) ───────────────────
 * The picker opens beside the form card or the container drawer's icon,
 * fixed so no overflow cuts it off. Suggestions sit under the card's host.
 */
.bookmark-form-modal-body .bookmark-form-card-menu-rule {
    margin: 4px 0;
    border: 0;
    border-top: 1px solid var(--border-secondary);
}

.icon-set-suggest {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 6px;
    margin-top: 6px;
}
.icon-set-suggest[hidden] { display: none; }
.icon-set-suggest-label {
    font-size: var(--text-2xs, 0.7rem);
    color: var(--text-tertiary);
}
.icon-set-suggest-chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 2px 8px 2px 4px;
    border: 1px solid var(--border-primary);
    border-radius: 999px;
    background: var(--background-primary);
    color: var(--text-primary);
    font: inherit;
    font-size: var(--text-xs, 0.75rem);
    cursor: pointer;
}
.icon-set-suggest-chip img { width: 16px; height: 16px; object-fit: contain; }
.icon-set-suggest-more { padding-left: 8px; color: var(--text-secondary); }
.icon-set-suggest-chip:hover,
.icon-set-suggest-chip:focus-visible {
    border-color: var(--accent-primary);
    outline: none;
}
.icon-set-suggest-chip:disabled { opacity: 0.6; cursor: progress; }

.icon-set-picker {
    position: fixed;
    /* Above the container drawer's host (1405) when it opens from there. */
    z-index: 1500;
    width: min(440px, calc(100vw - 16px));
    padding: 10px;
    border: 1px solid var(--border-primary);
    border-radius: var(--layout-radius-sm, 8px);
    background: var(--background-modal, var(--background-primary));
    color: var(--text-primary);
    box-shadow: 0 14px 40px rgba(0, 0, 0, 0.35);
}
.icon-set-picker[aria-busy="true"] { cursor: progress; }
.icon-set-picker-search { display: flex; align-items: center; gap: 8px; }
.icon-set-picker-search input { flex: 1; min-width: 0; }
.icon-set-picker-count {
    font-size: var(--text-2xs, 0.7rem);
    color: var(--text-tertiary);
    white-space: nowrap;
}
.icon-set-picker-grid {
    display: grid;
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: 6px;
    max-height: 236px;
    margin-top: 8px;
    padding: 2px;
    overflow: auto;
    outline: none;
}
.icon-set-picker-grid:empty { display: none; }
.icon-set-picker-option {
    display: grid;
    justify-items: center;
    gap: 3px;
    min-width: 0;
    padding: 7px 3px 5px;
    border: 1px solid transparent;
    border-radius: var(--radius-2, 6px);
    background: var(--background-primary);
    cursor: pointer;
}
.icon-set-picker-option img { width: 30px; height: 30px; object-fit: contain; }
.icon-set-picker-option span {
    max-width: 100%;
    overflow: hidden;
    font-size: var(--text-2xs, 0.7rem);
    color: var(--text-secondary);
    text-overflow: ellipsis;
    white-space: nowrap;
}
.icon-set-picker-option[aria-selected="true"] {
    border-color: var(--accent-primary);
    background: color-mix(in srgb, var(--accent-primary) 10%, var(--background-primary));
}
.icon-set-picker-grid:focus-visible .icon-set-picker-option[aria-selected="true"] {
    box-shadow: 0 0 0 1px var(--accent-primary);
}
.icon-set-picker-option[aria-selected="true"] span { color: var(--text-primary); }
.icon-set-picker-message {
    margin: 10px 4px 2px;
    font-size: var(--text-xs, 0.75rem);
    color: var(--text-secondary);
}
.icon-set-picker-message[hidden] { display: none; }
.icon-set-picker-foot { display: flex; gap: 6px; margin-top: 8px; min-height: 0; }
.icon-set-picker-variants { display: flex; gap: 4px; }
.icon-set-picker-variant {
    display: grid;
    place-items: center;
    width: 30px;
    height: 30px;
    padding: 0;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-2, 6px);
    background: var(--background-primary);
    cursor: pointer;
}
.icon-set-picker-variant img { width: 20px; height: 20px; object-fit: contain; }
/* Each variant shown on the background it is drawn for. */
.icon-set-picker-variant--light { background: #11151c; }
.icon-set-picker-variant--dark { background: #eef1f5; }
.icon-set-picker-variant[aria-checked="true"] {
    border-color: var(--accent-primary);
    box-shadow: 0 0 0 1px var(--accent-primary);
}
.icon-set-picker-credits {
    margin: 8px 0 0;
    padding-top: 8px;
    border-top: 1px solid var(--border-secondary);
    font-size: var(--text-2xs, 0.7rem);
    color: var(--text-tertiary);
}
.icon-set-picker-input {
    width: 100%;
    padding: 6px 9px;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-2, 6px);
    background: var(--background-primary);
    color: var(--text-primary);
    font: inherit;
}
.icon-set-picker-input:focus-visible {
    outline: 2px solid color-mix(in srgb, var(--accent-primary) 55%, transparent);
    outline-offset: 0;
}
.icon-set-picker-letter {
    display: grid;
    place-items: center;
    width: 30px;
    height: 30px;
    border-radius: var(--radius-2, 6px);
    background: var(--background-secondary, rgba(128, 128, 128, 0.15));
    color: var(--text-tertiary);
    font-size: 0.8rem;
}

/* ==== css/search-commands-new.css ==== */
/* New Bookmark Modal Styles */

/* The 520px cap used to cut the form off mid-way, so the fields between Tags
   and "More options" — availability checking among them — sat below the fold
   and had to be scrolled to before they could be seen at all. 760px shows the
   whole form down to the collapsed section on a normal window, while 90vh still
   keeps it inside a short one. The body scrolls either way. */
.modal-new-bookmark {
    max-width: 560px;
    width: min(560px, 92vw);
    padding: 0;
    overflow: hidden;
    max-height: min(90vh, 760px);
    display: flex;
    flex-direction: column;
}

.modal-new-bookmark.nbm-mobile-wizard {
    max-height: 90vh;
}

/* ── Header ── */
.nbm-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3, 0.75rem);
    padding: var(--space-3, 0.65rem) var(--space-4, 1rem);
    border-bottom: 1px solid var(--border-primary);
    background: var(--background-secondary);
    flex-shrink: 0;
}

.nbm-title {
    font-size: var(--font-size-text);
    font-weight: var(--font-weight-semibold);
    color: var(--text-primary);
}

.nbm-header-actions {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    flex-shrink: 0;
}

/* ── Form ── */
.new-bookmark-form {
    display: flex;
    flex-direction: column;
    overflow-y: auto;
}

.nbm-section {
    padding: var(--space-3, 0.65rem) var(--space-4, 1rem);
    border-bottom: 1px solid var(--border-primary);
    display: flex;
    flex-direction: column;
    gap: var(--space-1, 0.3rem);
}

.nbm-section-row {
    flex-direction: row;
    align-items: flex-end;
    gap: var(--space-3, 0.75rem);
    flex-wrap: wrap;
}

.nbm-section-toggles {
    flex-direction: row;
    gap: 1.25rem;
    align-items: center;
    flex-wrap: wrap;
}

/* Availability check and Shortcut share one row: the mode pills leave a column
   free beside them that the short input fits into exactly, which is what lets
   Shortcut sit above the fold without making the modal taller.
   Placed here beside .nbm-section-row rather than further down the file because
   it overrides .nbm-section's column direction at equal specificity — later
   rules win, so distance from that rule is what makes this work. */
.nbm-availability-row {
    flex-direction: row;
    align-items: flex-start;
    gap: var(--space-4, 1rem);
}

.nbm-availability-main {
    flex: 1 1 auto;
    min-width: 0;
}

.nbm-availability-aside {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-1, 0.3rem);
}

.nbm-availability-aside .nbm-input {
    width: 5.5rem;
}

/* Under the narrow breakpoint the two stack rather than squeezing the pills. */
@media (max-width: 520px) {
    .nbm-availability-row {
        flex-direction: column;
        gap: var(--space-2, 0.6rem);
    }

    .nbm-availability-aside {
        width: 100%;
    }
}

/* ── Labels ── */
.nbm-label {
    font-size: var(--font-size-controls);
    color: var(--text-secondary);
    font-weight: var(--font-weight-semibold);
    display: block;
}

.nbm-label-hint {
    font-weight: var(--font-weight-normal, 400);
    color: var(--text-tertiary);
    font-size: calc(var(--font-size-controls) * 0.9);
    margin-left: 0.3em;
}

/* ── Inputs ── */
.nbm-input {
    width: 100%;
    padding: var(--space-1-5, 0.38rem) var(--space-2, 0.5rem);
    border: 1px solid var(--border-primary);
    background: var(--background-primary);
    color: var(--text-primary);
    font-family: inherit;
    font-size: var(--font-size-text);
    transition: border-color 0.15s ease;
    box-sizing: border-box;
}

.nbm-input:focus {
    outline: none;
    border-color: var(--text-secondary);
}

.nbm-input::placeholder {
    color: var(--text-secondary);
    opacity: 0.5;
}

.nbm-note {
    resize: vertical;
    min-height: 3.5rem;
    line-height: 1.5;
    font-family: inherit;
}

.nbm-shortcut {
    text-transform: uppercase;
    text-align: center;
    max-width: 5.5rem;
}

.nbm-conflict-hint {
    display: inline;
    font-size: var(--text-xs, 0.72rem);
    color: var(--accent-error);
    white-space: nowrap;
    flex-shrink: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

.nbm-conflict-hint[hidden] {
    display: none !important;
}

.nbm-shortcut.field-conflict {
    border-color: var(--accent-error);
}

.nbm-input.field-conflict {
    border-color: var(--accent-error);
}

.nbm-url-conflict-hint {
    display: block;
    margin-top: var(--space-1-5, 0.35rem);
}

/* Not a conflict: the same link on another page is allowed, and this only says
   where it is. Muted rather than red for exactly that reason. */
.nbm-elsewhere-hint {
    display: block;
    margin-top: var(--space-1-5, 0.35rem);
    font-size: var(--text-xs, 0.72rem);
    color: var(--text-secondary);
    overflow: hidden;
    text-overflow: ellipsis;
}

.nbm-elsewhere-hint[hidden] {
    display: none !important;
}

.nbm-file-label {
    cursor: pointer;
}

.nbm-file-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

/* ── Row columns ── */
.nbm-col {
    display: flex;
    flex-direction: column;
    gap: var(--space-1, 0.3rem);
    flex: 1;
    min-width: 0;
}

.nbm-col-narrow {
    flex: 0 0 auto;
    min-width: 0;
}

/* Inline "create new page/category" row inside the add-bookmark modal. */
.nbm-new-option {
    font-style: italic;
}

.nbm-inline-create {
    display: flex;
    flex-direction: column;
    gap: var(--space-1, 0.3rem);
    margin-top: var(--space-1-5, 0.4rem);
    padding: var(--space-2, 0.5rem);
    border: 1px dashed var(--accent-success, #34d399);
    border-radius: var(--radius-5, 8px);
    background: color-mix(in srgb, var(--accent-success, #34d399) 8%, transparent);
}

/* The [hidden] attribute must win over the display above. */
.nbm-inline-create[hidden] {
    display: none;
}

.nbm-inline-create-hint {
    font-size: var(--text-sm, 0.78rem);
    font-weight: 600;
    color: var(--text-secondary, #9ca3af);
    letter-spacing: 0.02em;
}

.nbm-inline-create-row {
    display: flex;
    align-items: center;
    gap: var(--space-1-5, 0.35rem);
}

.nbm-inline-create-row .nbm-input {
    flex: 1 1 auto;
    min-width: 0;
}

.nbm-inline-create-ok,
.nbm-inline-create-cancel {
    flex: 0 0 auto;
    width: 2.1rem;
    height: 2.1rem;
    border-radius: var(--radius-5, 8px);
    border: 1px solid transparent;
    cursor: pointer;
    font-size: var(--text-md, 1rem);
    font-weight: 700;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: filter 0.15s ease, background 0.15s ease;
}

.nbm-inline-create-ok {
    background: var(--accent-success, #34d399);
    color: #06281d;
}

.nbm-inline-create-cancel {
    background: var(--background-secondary, rgba(128, 128, 128, 0.12));
    color: var(--text-secondary, #9ca3af);
    border-color: var(--border-primary, rgba(128, 128, 128, 0.3));
}

.nbm-inline-create-ok:hover {
    filter: brightness(1.08);
}

.nbm-inline-create-cancel:hover {
    color: var(--accent-error, #fb7185);
    border-color: var(--accent-error, #fb7185);
}

/* ── More options ── */
.nbm-more-options {
    border-bottom: 1px solid var(--border-primary);
}

.nbm-more-options > summary {
    padding: var(--space-2, 0.55rem) var(--space-4, 1rem);
    cursor: pointer;
    font-size: var(--font-size-controls);
    font-weight: var(--font-weight-semibold);
    color: var(--text-secondary);
    list-style: none;
    user-select: none;
}

.nbm-more-options > summary::-webkit-details-marker {
    display: none;
}

.nbm-more-options > summary::before {
    content: '▸ ';
    display: inline-block;
    transition: transform 0.15s ease;
}

.nbm-more-options[open] > summary::before {
    transform: rotate(90deg);
}

.nbm-more-content {
    display: flex;
    flex-direction: column;
    border-top: 1px solid var(--border-primary);
}

.nbm-more-content .nbm-section {
    border-bottom: none;
}

.nbm-more-content .nbm-section + .nbm-section {
    border-top: 1px solid var(--border-primary);
}

.nbm-more-content .bookmark-form-preview-section {
    padding: var(--space-3, 0.65rem) var(--space-4, 1rem);
    gap: var(--space-3, 0.65rem);
}

/* ── Mobile wizard ── */
.nbm-wizard-nav {
    display: none;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    padding: var(--space-2, 0.5rem) var(--space-4, 1rem);
    border-bottom: 1px solid var(--border-primary);
    background: var(--background-secondary);
    flex-shrink: 0;
}

.nbm-mobile-wizard .nbm-wizard-nav {
    display: flex;
}

.nbm-wizard-step {
    flex: 1;
    text-align: center;
    font-size: var(--text-xs, 0.72rem);
    font-weight: var(--font-weight-semibold);
    color: var(--text-tertiary);
    padding: var(--space-1, 0.25rem) var(--space-1-5, 0.35rem);
    border: 1px solid transparent;
}

.nbm-wizard-step.is-active {
    color: var(--text-primary);
    border-color: var(--border-primary);
    background: var(--background-primary);
}

.nbm-wizard-step.is-done {
    color: var(--text-secondary);
}

.nbm-mobile-wizard.nbm-wizard-step-2 .nbm-wizard-step-1-panel {
    display: none;
}

.nbm-mobile-wizard .nbm-section.nbm-wizard-step-2-panel {
    display: none;
}

.nbm-mobile-wizard.nbm-wizard-step-2 .nbm-section.nbm-wizard-step-2-panel {
    display: flex;
}

.nbm-mobile-wizard .nbm-more-options {
    display: none;
}

.nbm-mobile-wizard.nbm-wizard-step-2 .nbm-more-options {
    display: block;
}

.nbm-wizard-only {
    display: none;
}

.nbm-mobile-wizard .nbm-wizard-only {
    display: inline-flex;
}

.nbm-mobile-wizard.nbm-wizard-step-1 #new-bookmark-create {
    display: none;
}

.nbm-mobile-wizard.nbm-wizard-step-1 #new-bookmark-wizard-back {
    display: none;
}

.nbm-mobile-wizard.nbm-wizard-step-2 #new-bookmark-wizard-next {
    display: none;
}

.nbm-mobile-wizard:not(.nbm-wizard-step-1):not(.nbm-wizard-step-2) #new-bookmark-wizard-back,
.nbm-mobile-wizard:not(.nbm-wizard-step-1):not(.nbm-wizard-step-2) #new-bookmark-wizard-next {
    display: none;
}

/* ── URL row ── */
.nbm-url-row {
    display: flex;
    gap: var(--space-1-5, 0.4rem);
    align-items: center;
}

.nbm-url-row .nbm-input {
    flex: 1;
    min-width: 0;
}

/* ── Icon row ── */
.nbm-icon-row {
    display: flex;
    gap: var(--space-1-5, 0.4rem);
    align-items: center;
}

.nbm-icon-preview {
    width: 2rem;
    height: 2rem;
    border: 1px solid var(--border-primary);
    background: var(--background-secondary);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    flex-shrink: 0;
    color: var(--text-tertiary);
}

.nbm-icon-preview img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

.nbm-icon-preview-empty {
    font-size: var(--text-xs, 0.7rem);
    line-height: 1;
}

.nbm-icon-row .nbm-input {
    flex: 1;
    min-width: 0;
}

.nbm-icon-clear {
    flex-shrink: 0;
    min-width: 2rem;
    padding: 0 var(--space-2, 0.45rem);
}

.nbm-icon-state {
    font-size: var(--font-size-controls);
    color: var(--text-tertiary);
    min-height: 1em;
    padding-top: var(--space-hairline, 0.1rem);
}

/* ── Toggles ── */
.nbm-toggle-label {
    display: flex;
    align-items: center;
    gap: var(--space-1-5, 0.4rem);
    cursor: pointer;
    font-size: var(--font-size-controls);
    color: var(--text-secondary);
    user-select: none;
}

.nbm-toggle-label input[type="checkbox"] {
    appearance: none;
    -webkit-appearance: none;
    width: 14px;
    height: 14px;
    border: 1px solid var(--border-primary);
    border-radius: 0;
    cursor: pointer;
    position: relative;
    margin: 0;
    flex-shrink: 0;
    background: var(--background-primary);
}

.nbm-toggle-label input[type="checkbox"]:checked {
    border-color: var(--accent-success);
}

.nbm-toggle-label input[type="checkbox"]:checked::before {
    content: 'X';
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: var(--font-size-checkbox);
    font-weight: var(--font-weight-black);
    line-height: 1;
    color: var(--accent-success);
}

/* ── Buttons ── */
.nbm-btn {
    padding: var(--space-1-5, 0.38rem) var(--space-3, 0.75rem);
    border: 1px solid var(--border-primary);
    background: transparent;
    color: var(--text-primary);
    font-family: inherit;
    font-size: var(--font-size-controls);
    font-weight: var(--font-weight-semibold);
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
    white-space: nowrap;
    flex-shrink: 0;
}

.nbm-btn:hover {
    background: var(--background-secondary);
}

.nbm-btn-primary {
    border-color: var(--text-secondary);
    color: var(--text-primary);
}

.nbm-btn-primary:hover {
    background: var(--background-secondary);
}

.nbm-btn-secondary {
    color: var(--text-secondary);
}

/* "Create + New" — a distinct accent so it reads apart from the neutral primary
   Create button: it saves and immediately reopens the form for another. */
.nbm-btn-create-another {
    border-color: var(--accent-success, #34d399);
    color: var(--accent-success, #34d399);
}

.nbm-btn-create-another:hover {
    background: color-mix(in srgb, var(--accent-success, #34d399) 12%, transparent);
    color: var(--accent-success, #34d399);
}

/* ── Footer ── */
.nbm-footer {
    display: flex;
    gap: var(--space-2, 0.5rem);
    justify-content: flex-end;
    padding: var(--space-3, 0.65rem) var(--space-4, 1rem);
    background: var(--background-secondary);
    flex-shrink: 0;
}

/* ── Responsive ── */
@media (max-width: 580px) {
    .modal-new-bookmark {
        width: min(560px, 96vw);
    }

    .nbm-section-row {
        flex-direction: column;
        gap: var(--space-2, 0.5rem);
    }

    .nbm-col-narrow {
        flex: 1;
    }
}

/* ==== css/bookmark-form-preview.css ==== */
/* Shared bookmark form preview (dashboard tile + link preview card) */

.bookmark-form-preview-section {
    display: flex;
    flex-direction: column;
    gap: var(--space-3, 0.75rem);
}

.bookmark-form-preview-block {
    display: flex;
    flex-direction: column;
    gap: var(--space-1-5, 0.35rem);
}

.bookmark-form-preview-block-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2, 0.5rem);
    flex-wrap: wrap;
}

.bookmark-form-preview-label {
    font-size: var(--font-size-controls, 0.8125rem);
    font-weight: var(--font-weight-semibold, 600);
    color: var(--text-secondary);
}

.bookmark-form-preview-hint {
    font-size: var(--text-xs, 0.72rem);
    color: var(--text-tertiary);
    font-weight: var(--font-weight-normal, 400);
}

.bookmark-form-preview-empty {
    font-size: var(--font-size-controls, 0.8125rem);
    color: var(--text-tertiary);
    margin: 0;
    line-height: 1.45;
}

.bookmark-detail-link-preview-actions {
    display: flex;
    gap: var(--space-1-5, 0.35rem);
    flex-shrink: 0;
}

.config-bookmark-preview-hover-zone {
    position: relative;
    max-width: 100%;
    outline: none;
}

.config-bookmark-preview {
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-4, 6px);
    background: color-mix(in srgb, var(--background-primary) 55%, transparent);
    padding: var(--space-1-5, 0.35rem) var(--space-2, 0.5rem);
}

.config-bookmark-preview--compact {
    max-width: 100%;
    cursor: default;
}

.config-bookmark-preview-popover {
    display: none;
    position: absolute;
    left: 0;
    top: calc(100% + 0.35rem);
    z-index: 40;
    min-width: min(22rem, 100%);
    max-width: min(28rem, calc(100vw - 2rem));
    padding: var(--space-2, 0.5rem) var(--space-3, 0.65rem);
    border: 1px solid color-mix(in srgb, var(--accent-primary) 35%, var(--border-primary));
    border-radius: var(--radius-5, 8px);
    background: var(--background-primary);
    box-shadow: 0 10px 28px color-mix(in srgb, var(--background-primary) 20%, transparent),
        0 0 0 1px color-mix(in srgb, var(--accent-primary) 12%, transparent);
    pointer-events: none;
}

.config-bookmark-preview-hover-zone:hover .config-bookmark-preview-popover,
.config-bookmark-preview-hover-zone:focus-within .config-bookmark-preview-popover {
    display: block;
}

.config-bookmark-preview-hover-zone:hover .config-bookmark-preview--compact,
.config-bookmark-preview-hover-zone:focus-within .config-bookmark-preview--compact {
    border-color: color-mix(in srgb, var(--accent-primary) 45%, var(--border-primary));
}

.config-bookmark-preview-tile.bookmark-link {
    display: grid;
    grid-template-columns: minmax(1rem, max-content) minmax(0, 1fr) max-content;
    align-items: center;
    column-gap: var(--space-1-5, 0.35rem);
    padding: var(--space-hairline, 0.1rem) 0;
    min-height: 1.55rem;
    font-size: var(--font-size-bookmark, 0.8125rem);
    font-weight: var(--font-weight-semibold, 600);
    color: var(--text-primary);
    white-space: nowrap;
    max-width: 100%;
}

.config-bookmark-preview-tile .bookmark-lead {
    grid-column: 1;
    grid-row: 1;
    display: inline-flex;
    align-items: center;
    min-width: 1.1em;
    cursor: default;
}

.config-bookmark-preview-tile .bookmark-open {
    grid-column: 2;
    grid-row: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: var(--space-1, 0.25rem);
    overflow: hidden;
}

.config-bookmark-preview-tile .bookmark-text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

.config-bookmark-preview-tile--expanded .bookmark-text {
    overflow: visible;
    text-overflow: unset;
    white-space: normal;
    word-break: break-word;
}

.config-bookmark-preview-tile--expanded.bookmark-link {
    white-space: normal;
    min-height: 2rem;
    padding: var(--space-hairline, 0.15rem) 0;
}

.config-bookmark-preview-tile--expanded .bookmark-open {
    flex-wrap: wrap;
    overflow: visible;
}

.config-link-preview-card {
    display: grid;
    grid-template-columns: 5.5rem 1fr;
    gap: var(--space-3, 0.65rem);
    align-items: start;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-5, 8px);
    padding: var(--space-2, 0.55rem) var(--space-3, 0.65rem);
    background: color-mix(in srgb, var(--background-primary) 55%, transparent);
    margin-top: var(--space-1, 0.25rem);
}

.config-link-preview-card-image-wrap {
    width: 5.5rem;
    height: 3.25rem;
    border-radius: var(--radius-3, 4px);
    overflow: hidden;
    background: var(--background-secondary);
    border: 1px solid var(--border-secondary);
}

.config-link-preview-card-image {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.config-link-preview-card-body {
    min-width: 0;
}

.config-link-preview-card-title {
    font-weight: var(--font-weight-semibold, 600);
    font-size: var(--font-size-text);
    color: var(--text-primary);
    line-height: 1.35;
    margin-bottom: var(--space-1, 0.2rem);
}

.config-link-preview-card-desc {
    font-size: var(--font-size-controls);
    color: var(--text-secondary);
    line-height: 1.45;
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.config-link-preview-card-domain {
    margin-top: var(--space-1-5, 0.35rem);
    font-size: var(--text-xs, 0.75rem);
    color: var(--text-tertiary);
}

.config-link-preview-card:not(:has(.config-link-preview-card-image-wrap)) {
    grid-template-columns: 1fr;
}

.config-bookmark-preview-tile .bookmark-shortcut {
    grid-column: 3;
    grid-row: 1;
    font-size: 0.85em;
    opacity: 0.85;
    text-align: right;
}

.config-bookmark-preview-tile .bookmark-icon-slot {
    width: 1.1em;
    height: 1.1em;
}

.config-bookmark-preview-tile .bookmark-icon-slot--empty {
    border: 1px dashed var(--border-secondary);
    border-radius: var(--radius-2, 3px);
    opacity: 0.45;
}

.config-bookmark-preview-tile .bookmark-icon {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

.config-bookmark-preview-tile .status-text {
    flex-shrink: 0;
    font-size: 0.75em;
    font-weight: var(--font-weight-normal, 400);
}

.config-bookmark-preview-tile .bookmark-superscript-badge {
    flex-shrink: 0;
    width: 0.85em;
    height: 0.85em;
}

.config-bookmark-preview-tile .bookmark-superscript-badge svg {
    width: 100%;
    height: 100%;
    fill: currentColor;
}

.modal-new-bookmark .bookmark-form-preview-section {
    margin: var(--space-1, 0.25rem) 0 var(--space-2, 0.5rem);
}

.modal-new-bookmark .config-link-preview-card {
    grid-template-columns: 4.5rem 1fr;
}

.modal-new-bookmark .config-link-preview-card-image-wrap {
    width: 4.5rem;
    height: 2.75rem;
}

/* Compact preview strip (primary add-bookmark view) */
.bookmark-form-preview-strip {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.55rem);
    padding: var(--space-2, 0.45rem) var(--space-2, 0.55rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-4, 6px);
    background: color-mix(in srgb, var(--background-primary) 55%, transparent);
    min-height: 2.35rem;
}

.bookmark-form-preview-strip-icon {
    width: 1.35rem;
    height: 1.35rem;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}

.bookmark-form-preview-strip-icon img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

.bookmark-form-preview-strip-icon-empty {
    display: block;
    width: 100%;
    height: 100%;
    border: 1px dashed var(--border-secondary);
    border-radius: var(--radius-2, 3px);
    opacity: 0.45;
}

.bookmark-form-preview-strip-meta {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-hairline, 0.05rem);
}

.bookmark-form-preview-strip-title {
    font-size: var(--font-size-text);
    font-weight: var(--font-weight-semibold, 600);
    color: var(--text-primary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.bookmark-form-preview-strip-domain {
    font-size: var(--text-xs, 0.72rem);
    color: var(--text-tertiary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.bookmark-form-preview-strip-tile {
    flex-shrink: 0;
    max-width: 42%;
    min-width: 0;
    overflow: hidden;
}

.bookmark-form-preview-strip-tile .config-bookmark-preview-tile.bookmark-link {
    font-size: var(--text-xs, 0.72rem);
    min-height: 1.35rem;
    padding: 0;
}

.nbm-section-compact-preview {
    padding-top: var(--space-1-5, 0.35rem);
    padding-bottom: var(--space-1-5, 0.35rem);
}

.ext-preview-section .config-link-preview-card {
    grid-template-columns: 3.5rem 1fr;
    padding: var(--space-2, 0.45rem) var(--space-2, 0.5rem);
}

.ext-preview-section .config-link-preview-card-image-wrap {
    width: 3.5rem;
    height: 2.25rem;
}

.ext-preview-section .bookmark-form-preview-block-header {
    margin-top: var(--space-1-5, 0.35rem);
}

/* The feed behind this bookmark, when Fresh knows of one.
   A quiet line under the preview: it answers "why does this one never say
   anything", which is a question asked once, not a fact to read every time. */
.bookmark-form-feed-line {
    display: flex;
    align-items: baseline;
    gap: var(--space-1-5, 0.4rem);
    margin: var(--space-2, 0.5rem) 0 0;
    font-size: var(--text-sm, 0.8rem);
    color: var(--text-secondary, #666);
    overflow: hidden;
}

.bookmark-form-feed-label {
    flex: 0 0 auto;
    color: var(--text-tertiary, #888);
}

.bookmark-form-feed-url {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: var(--text-xs, 0.75rem);
}

/* ==== css/check-mode-control.css ==== */
/* ─── Availability check mode (shared) ───────────────────────────────────────
   The three-way Off / Periodic / Monitor control, used by both surfaces that
   offer the choice: the config bookmark panel and the dashboard's add-bookmark
   modal. It lives in its own file rather than in config-bookmarks-splitview.css
   because the dashboard does not load that stylesheet — copying the rules there
   is how the two would end up looking different.

   The .bookmark-detail-* names are kept even outside the config panel: the
   markup is identical on both surfaces on purpose, so one set of rules can
   style them both.
   ------------------------------------------------------------------------- */

/* Needed by the block below, and small enough to travel with it rather than
   pull the whole config panel stylesheet onto the dashboard. */
.bookmark-detail-label {
    font-size: var(--font-size-controls);
    color: var(--text-secondary);
    font-weight: var(--font-weight-semibold);
    display: block;
}

.bookmark-detail-field-hint {
    display: block;
    font-size: var(--font-size-small, 0.75rem);
    color: var(--text-tertiary);
    margin-top: var(--space-1, 0.2rem);
}

.bookmark-detail-checkmode-header {
    display: flex;
    align-items: center;
    gap: var(--space-1-5, 0.35rem);
}

.bookmark-detail-checkmode-options {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1, 0.25rem);
    margin-top: var(--space-1, 0.3rem);
}

.bookmark-detail-checkmode-input {
    position: absolute;
    width: 1px !important;
    height: 1px !important;
    margin: -1px !important;
    padding: 0 !important;
    border: 0 !important;
    opacity: 0 !important;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    pointer-events: none;
    appearance: none;
    -webkit-appearance: none;
}

.bookmark-detail-checkmode-option {
    padding: var(--space-1, 0.25rem) var(--space-2, 0.6rem);
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-pill, 999px);
    background: var(--background-primary);
    color: var(--text-secondary);
    font-size: var(--font-size-controls);
    line-height: 1.2;
    cursor: pointer;
    user-select: none;
    transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
}

.bookmark-detail-checkmode-option:hover {
    border-color: color-mix(in srgb, var(--accent-primary) 45%, var(--border-secondary));
    color: var(--text-primary);
}

.bookmark-detail-checkmode-input:checked + .bookmark-detail-checkmode-option {
    border-color: var(--accent-primary);
    background: color-mix(in srgb, var(--accent-primary) 14%, var(--background-primary));
    color: var(--text-primary);
    font-weight: var(--font-weight-semibold);
}

.bookmark-detail-checkmode-input:focus-visible + .bookmark-detail-checkmode-option {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
}

.bookmark-detail-checkmode-info {
    width: 1.15rem;
    height: 1.15rem;
    padding: 0;
    border: 1px solid var(--border-secondary);
    border-radius: 50%;
    background: transparent;
    color: var(--text-tertiary);
    font-size: var(--text-xs, 0.7rem);
    font-style: italic;
    line-height: 1;
    cursor: pointer;
    flex: 0 0 auto;
}

.bookmark-detail-checkmode-info:hover {
    border-color: var(--accent-primary);
    color: var(--text-primary);
}

.bookmark-detail-checkmode .bookmark-detail-field-hint {
    margin-top: var(--space-1-5, 0.35rem);
}

.bookmark-detail-checkmode .custom-select-wrapper,
.bookmark-detail-checkmode .bookmark-detail-toggle-select {
    flex: 0 1 auto;
    width: auto;
    min-width: 0;
}

@media (prefers-reduced-motion: reduce) {
    .bookmark-detail-checkmode-option {
        transition: none;
    }
}

/* The modal is narrower than the config panel, so the row wraps rather than
   pushing the interval select off the edge. */
.modal-new-bookmark .bookmark-detail-checkmode-options {
    row-gap: var(--space-1-5, 0.35rem);
}

/* ─── Explainer stacking ─────────────────────────────────────────────────────
   The (i) explainer is opened from surfaces that are themselves modals — the
   add-bookmark modal, the inline editor — and every overlay in the app sits at
   z-index 2000. A tie is broken by DOM order, and #app-modal is created first,
   so without this the explanation opens *behind* the form that asked for it.
   ------------------------------------------------------------------------- */
#app-modal.modal-overlay:has(.check-mode-explain-modal) {
    z-index: 2100;
}


/* ─── Icon toggle (shared) ───────────────────────────────────────────────────
   The compact checkbox-as-pill used for Pinned. Moved out of config-lists.css
   so the dashboard's add-bookmark modal can use the same control — that page
   does not load the config stylesheets, and a second copy is how the two would
   drift apart.
   ------------------------------------------------------------------------- */
.icon-toggle {
    position: relative;
    display: inline-flex;
    align-items: center;
}

.icon-toggle input[type="checkbox"] {
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
    pointer-events: none;
}

.icon-toggle-indicator {
    width: 1.75rem;
    height: 1.75rem;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-3, 4px);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--text-secondary);
    background: var(--background-primary);
    cursor: pointer;
    position: relative;
}

.icon-toggle-indicator svg {
    width: 0.95rem;
    height: 0.95rem;
    fill: currentColor;
    display: block;
}

.icon-toggle input[type="checkbox"]:checked + .icon-toggle-indicator {
    color: var(--text-primary);
    border-color: var(--text-secondary);
}

.icon-toggle input[type="checkbox"]:focus-visible + .icon-toggle-indicator {
    outline: 2px solid var(--text-secondary);
    outline-offset: 2px;
}

.icon-toggle-indicator[data-tooltip]::after {
    content: attr(data-tooltip);
    position: absolute;
    bottom: calc(100% + 6px);
    left: 50%;
    transform: translateX(-50%);
    padding: var(--space-1, 0.2rem) var(--space-2, 0.45rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-3, 4px);
    background: var(--background-secondary);
    color: var(--text-primary);
    font-size: var(--text-xs, 0.72rem);
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.12s ease;
    z-index: 15;
}

.icon-toggle-indicator[data-tooltip]:hover::after,
.icon-toggle input[type="checkbox"]:focus-visible + .icon-toggle-indicator::after {
    opacity: 1;
}

/* The label beside the pill, and the modal's own spacing for it. */
.bookmark-detail-toggle {
    gap: var(--space-1-5, 0.4rem);
    cursor: pointer;
}

.bookmark-detail-toggle-label {
    font-size: var(--font-size-controls);
    color: var(--text-secondary);
}

.nbm-pin-toggle {
    margin-top: var(--space-hairline, 0.15rem);
}

/* ==== css/search-commands-note.css ==== */
/* :note command modal (AppModal + note-command-modal-inner) */

.modal.note-command-modal-inner {
    max-width: 480px;
    width: min(480px, 92vw);
    padding: 0;
    overflow: hidden;
}

.modal.note-command-modal-inner .modal-header {
    padding: var(--space-3, 0.65rem) var(--space-4, 1rem);
    border-bottom: 1px solid var(--border-primary);
    background: var(--background-secondary);
}

.modal.note-command-modal-inner .modal-title {
    font-size: var(--font-size-text);
    font-weight: var(--font-weight-semibold);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.modal.note-command-modal-inner .modal-body {
    padding: 0;
}

.modal.note-command-modal-inner .modal-actions {
    padding: var(--space-2, 0.6rem) var(--space-4, 1rem);
    border-top: 1px solid var(--border-primary);
    background: var(--background-secondary);
}

.note-cmd-body {
    padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
}

.note-cmd-textarea {
    width: 100%;
    box-sizing: border-box;
    background: var(--background-primary);
    color: var(--text-primary);
    border: 1px solid var(--border-primary);
    border-radius: var(--border-radius-sm, 3px);
    padding: var(--space-2, 0.6rem) var(--space-3, 0.75rem);
    font-family: inherit;
    font-size: var(--font-size-text);
    line-height: 1.5;
    resize: vertical;
    outline: none;
    transition: border-color 0.15s;
}

.note-cmd-textarea:focus {
    border-color: var(--accent-info, #60a5fa);
}

.note-cmd-textarea::placeholder {
    color: var(--text-tertiary);
    opacity: 0.7;
}

.note-cmd-hint {
    margin: var(--space-2, 0.5rem) 0 0;
    font-size: var(--font-size-small);
    color: var(--text-secondary);
}

/* ==== css/status.css ==== */
/* Status Monitoring Styles */

/* Theme-aware status tokens — tuned for contrast on every theme */
body:not(.status-color-lock) {
    --status-offline-fg: var(--accent-error);
    --status-checking-fg: var(--accent-warning);

    --status-offline-bg: color-mix(in srgb, var(--accent-error) 20%, var(--background-primary));
    --status-checking-bg: color-mix(in srgb, var(--accent-warning) 14%, var(--background-primary));

    --status-offline-bg-active: color-mix(in srgb, var(--accent-error) 30%, var(--background-primary));
    --status-checking-bg-active: color-mix(in srgb, var(--accent-warning) 22%, var(--background-primary));

    --status-offline-bar: var(--accent-error);
    --status-offline-ring: color-mix(in srgb, var(--accent-error) 55%, transparent);
    --status-checking-ring: color-mix(in srgb, var(--accent-warning) 48%, transparent);
}

/* Status indicators — colour/background only; avoid opacity/transform so rows keep stable height */
.bookmark-link.status-checking {
    transition: color var(--transition-normal), background-color var(--transition-normal), box-shadow var(--transition-normal);
}

.bookmark-link.status-online {
    position: relative;
    transition: color var(--transition-normal), background-color var(--transition-normal), box-shadow var(--transition-normal);
}

.bookmark-link.status-offline {
    position: relative;
    transition: color var(--transition-normal), background-color var(--transition-normal), box-shadow var(--transition-normal);
}

/* Status colors — offline/checking rows only (idle); hover/selection uses bookmark-row chrome */
body:not(.status-color-lock) .bookmarks-list[data-bookmarks-list="true"]:not([data-smart-collection="true"]) .bookmark-link.status-offline:not(:hover):not(.keyboard-selected):not(:focus-within):not(.bookmark-inline-editing) {
    color: var(--status-offline-fg, var(--accent-error, #ef4444));
    background: var(--status-offline-bg, color-mix(in srgb, var(--accent-error, #ef4444) 24%, var(--background-primary)));
    box-shadow: inset 3px 0 0 var(--status-offline-bar, var(--accent-error, #ef4444));
}

body:not(.status-color-lock) .bookmarks-list[data-bookmarks-list="true"]:not([data-smart-collection="true"]) .bookmark-link.status-checking:not(:hover):not(.keyboard-selected):not(:focus-within):not(.bookmark-inline-editing) {
    color: var(--status-checking-fg, var(--accent-warning, #f59e0b));
    background: var(--status-checking-bg, color-mix(in srgb, var(--accent-warning, #f59e0b) 16%, var(--background-primary)));
}

body:not(.status-color-lock) .bookmarks-list[data-bookmarks-list="true"]:not([data-smart-collection="true"]) .bookmark-link.status-offline .bookmark-open,
body:not(.status-color-lock) .bookmarks-list[data-bookmarks-list="true"]:not([data-smart-collection="true"]) .bookmark-link.status-offline .bookmark-text,
body:not(.status-color-lock) .bookmarks-list[data-bookmarks-list="true"]:not([data-smart-collection="true"]) .bookmark-link.status-checking .bookmark-open,
body:not(.status-color-lock) .bookmarks-list[data-bookmarks-list="true"]:not([data-smart-collection="true"]) .bookmark-link.status-checking .bookmark-text {
    color: inherit;
}

/* Optional: preserve bookmark row colors while still showing status state/ping (idle only) */
body.status-color-lock .bookmark-link.status-online:not(:hover):not(.keyboard-selected):not(:focus-within):not(.bookmark-inline-editing),
body.status-color-lock .bookmark-link.status-offline:not(:hover):not(.keyboard-selected):not(:focus-within):not(.bookmark-inline-editing),
body.status-color-lock .bookmark-link.status-checking:not(:hover):not(.keyboard-selected):not(:focus-within):not(.bookmark-inline-editing) {
    color: inherit;
    background: none;
    box-shadow: none;
    font-weight: inherit;
}

body.status-color-lock .bookmark-link.status-online:not(:hover):not(.keyboard-selected):not(:focus-within) .status-text,
body.status-color-lock .bookmark-link.status-offline:not(:hover):not(.keyboard-selected):not(:focus-within) .status-text,
body.status-color-lock .bookmark-link.status-checking:not(:hover):not(.keyboard-selected):not(:focus-within) .status-text {
    color: var(--text-secondary);
}

/* Status text (ping time) — dedicated grid column; fixed width via bookmarks-list[data-show-ping] */
.status-text {
    opacity: 0.9;
    transition: color 0.3s ease;
    color: var(--text-secondary);
    min-width: 0;
}

.status-text.is-empty {
    visibility: hidden;
}

/* Online status ping time — row stays neutral; badge shows reachability when idle */
.bookmark-link.status-online:not(:hover):not(.keyboard-selected):not(:focus-within) .status-text {
    color: var(--accent-success);
    border-color: color-mix(in srgb, var(--accent-success) 30%, transparent);
}

/* Hover/selection: ping badge matches other row badges (full row uses bookmark chrome) */
.bookmark-link.status-online:is(:hover, .keyboard-selected, :focus-within):not(.bookmark-inline-editing) .status-text,
.bookmark-link.status-offline:is(:hover, .keyboard-selected, :focus-within):not(.bookmark-inline-editing) .status-text,
.bookmark-link.status-checking:is(:hover, .keyboard-selected, :focus-within):not(.bookmark-inline-editing) .status-text {
    color: var(--text-secondary);
    border-color: color-mix(in srgb, var(--accent-primary) 35%, var(--border-secondary));
    background: transparent;
}

/* Offline status text (idle only) */
.bookmark-link.status-offline:not(:hover):not(.keyboard-selected):not(:focus-within) .status-text {
    color: var(--accent-error);
    border-color: color-mix(in srgb, var(--accent-error) 30%, transparent);
}

/* Checking status text (idle only) */
.bookmark-link.status-checking:not(:hover):not(.keyboard-selected):not(:focus-within) .status-text {
    color: var(--accent-warning);
    border-color: color-mix(in srgb, var(--accent-warning) 30%, transparent);
}

/* Status indicator dots - HIDDEN */
.status-indicator {
    display: none;
}

/* Status indicator styles removed - indicator hidden */

@keyframes pulse {
    0%, 100% {
        opacity: 1;
    }
    50% {
        opacity: 0.5;
    }
}

/* Configuration toggle for individual bookmarks */
.bookmark-status-toggle {
    font-size: var(--font-size-controls);
}

.bookmark-status-toggle input[type="checkbox"] {
    width: 14px !important;
    height: 14px !important;
    margin-right: var(--space-1, 0.25rem);
}

.bookmark-status-toggle label {
    font-size: var(--font-size-controls);
    opacity: 0.8;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: var(--space-1, 0.25rem);
    color: var(--text-secondary);
}

/* Status monitoring settings group */
.status-settings-group {
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-5, 8px);
    padding: var(--space-4, 1rem);
    margin-top: var(--space-4, 1rem);
    background: var(--background-secondary);
}

.status-settings-info {
    margin-top: var(--space-2, 0.5rem);
    margin-left: var(--space-5, 1.625rem); /* Align with checkbox text */
}

.status-settings-info small {
    font-size: var(--font-size-controls);
    opacity: 0.7;
    line-height: 1.4;
}

/* Nested setting for ping display */
.status-settings-nested {
    margin-left: var(--space-5, 1.625rem);
    margin-top: var(--space-3, 0.75rem);
    padding-left: var(--space-4, 1rem);
    border-left: 2px solid var(--border-primary);
}

/* Refresh button for manual status check */
.status-refresh-btn {
    margin-left: var(--space-2, 0.5rem);
    padding: var(--space-1, 0.25rem) var(--space-2, 0.5rem);
    font-size: var(--font-size-controls);
    font-family: var(--font-family-main);
    border: 1px solid var(--border-primary);
    border-radius: var(--layout-radius-sm, 8px);
    cursor: pointer;
    transition: all 0.2s ease;
    background: var(--background-secondary);
    color: var(--text-secondary);
}

.status-refresh-btn:hover {
    background: var(--background-secondary);
    border-color: var(--text-secondary);
    color: var(--text-primary);
}

/* Status Loading Indicator */
.status-loading-indicator {
    display: none; /* Hidden by default */
    font-family: var(--font-family-main);
    font-size: var(--font-size-controls);
    font-weight: var(--font-weight-bold);
    color: var(--accent-warning);
    opacity: 0.8;
    margin-left: var(--space-4, 1rem);
    align-items: center;
    gap: var(--space-1, 0.25rem);
    white-space: nowrap;
}

.status-loading-indicator.show {
    display: flex;
}

.status-loading-text {
    font-family: var(--font-family-main);
    letter-spacing: 0.05em;
}

.status-loading-dots::after {
    content: '';
    animation: loading-dots 1.5s infinite;
    font-weight: var(--font-weight-bold);
}

@keyframes loading-dots {
    0%, 30% {
        content: '.';
    }
    31%, 60% {
        content: '..';
    }
    61%, 100% {
        content: '...';
    }
}

/* Responsive adjustments */
@media (max-width: 768px) {
    .status-loading-indicator {
        font-size: calc(var(--font-size-controls) * 0.9);
        margin-left: var(--space-2, 0.5rem);
    }
}

/* ── Monitor emphasis ───────────────────────────────────────────────────────
   How loudly a monitored bookmark announces itself, chosen in Behavior →
   Status & health and mirrored onto <body> by setupDOM.

   The row always carries data-check-mode="monitor"; only the emphasis differs,
   so switching modes is a repaint rather than a re-render.

     problems (default) — a healthy monitor looks like any other bookmark; only
                          an outage draws the eye, via the status-offline rules
                          above. Nothing extra is needed here.
     always             — an accent edge on every monitor, healthy or not.
     never              — monitoring stays in the Health view, including the
                          outage colouring the other two modes rely on.
*/
body[data-monitor-emphasis="always"] .bookmarks-list[data-bookmarks-list="true"]:not([data-smart-collection="true"]) .bookmark-link[data-check-mode="monitor"]:not(.status-offline):not(:hover):not(.keyboard-selected):not(:focus-within):not(.bookmark-inline-editing) {
    box-shadow: inset 3px 0 0 var(--monitor-emphasis-bar, var(--accent-primary));
}

/* Deliberately also suppresses the offline colouring: "never stand out" that
   still turned a row red would not be never. The Health view and the header
   badge remain the places an outage is reported. */
body[data-monitor-emphasis="never"] .bookmark-link[data-check-mode="monitor"]:not(:hover):not(.keyboard-selected):not(:focus-within):not(.bookmark-inline-editing) {
    color: inherit;
    background: none;
    box-shadow: none;
}

/* ==== css/select.css ==== */
/* Custom Terminal-Style Select Component */

.custom-select-wrapper {
    position: relative;
    display: inline-block;
    min-width: 0;
}

.page-selector-wrapper .custom-select-wrapper {
    min-width: 200px;
    width: 100%;
}

.custom-select {
    position: relative;
    cursor: pointer;
}

.custom-select-trigger {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-1-5, 0.375rem) var(--space-2, 0.5rem);
    border: none;
    border: 1px solid var(--border-primary);
    /* The surface the rows and the band are drawn on, not the page colour: a
       control painted in --background-primary reads as a hole in the view. */
    background: color-mix(in srgb, var(--background-secondary) 55%, transparent);
    color: var(--text-primary);
    font-size: var(--font-size-text);
    font-family: var(--font-family-main);
    cursor: pointer;
    user-select: none;
    transition: border-color 0.3s ease;
}

.custom-select-trigger:hover {
    opacity: 0.8;
}

.custom-select.open .custom-select-trigger {
    border-color: var(--text-secondary);
}

.custom-select-arrow {
    font-size: var(--font-size-controls);
    margin-left: var(--space-2, 0.5rem);
    transition: transform 0.2s ease;
}

.custom-select.open .custom-select-arrow {
    transform: rotate(180deg);
}

.custom-select-options {
    position: fixed;
    border: 1px solid var(--border-primary);
    max-height: 250px;
    overflow-y: auto;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.2s ease, visibility 0.2s ease;
    z-index: 9999;
    background-color: var(--background-primary);
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
}

.custom-select-options.is-open {
    opacity: 1;
    visibility: visible;
}

.custom-select-option {
    padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
    font-size: var(--font-size-text);
    font-family: var(--font-family-main);
    cursor: pointer;
    transition: background-color 0.15s ease;
    user-select: none;
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    color: var(--text-primary);
}

.custom-select-option:hover {
    background-color: var(--background-secondary);
}

.custom-select-option.selected {
    font-weight: var(--font-weight-semibold);
    color: var(--accent-success);
}

.custom-select-option.selected::before {
    content: '>';
    font-weight: var(--font-weight-black);
    margin-right: var(--space-1, 0.25rem);
}

.custom-select-option.highlighted {
    font-weight: var(--font-weight-semibold);
}

/* Scrollbar styling for options */
.custom-select-options::-webkit-scrollbar {
    width: 8px;
}

.custom-select-options::-webkit-scrollbar-track {
    background: var(--background-primary);
}

.custom-select-options::-webkit-scrollbar-thumb {
    background: var(--border-primary);
    border-radius: 0;
}

.custom-select-options::-webkit-scrollbar-thumb:hover {
    background: var(--text-secondary);
}

/* Hide original select */
.custom-select-wrapper select {
    display: none;
}

/* ==== css/font-size.css ==== */
/* Font Size System - CSS Variables */

/* Global font for the entire app */
:root {
    --font-family-main: 'Source Code Pro', monospace;
    font-family: var(--font-family-main);
}

/* Font Size Variables - Default (Medium/m - Level 3) */
:root {
    /* Main Categories - Only 5 core variables */
    --font-size-title: 3rem;           /* Dashboard title (big title) */
    --font-size-date: 1.125rem;        /* Date in header */
    --font-size-category: 1.125rem;    /* Category/column titles */
    --font-size-bookmark: 1.25rem;     /* Bookmark links */
    --font-size-controls: 0.875rem;    /* Tabs, buttons, config link */
    --font-size-text: 0.875rem;        /* General text (forms, config, body text, etc.) */
    /*
     * Captions, hints, and the word under a figure.
     *
     * Used in about forty places and defined in none of them until now, so
     * every rule that asked for it without spelling out a fallback was an
     * invalid declaration and the text rendered at whatever it inherited --
     * measured at 18px where the surrounding body text was also 18px. The
     * sixteen call sites that did carry a fallback are why it looked
     * inconsistent rather than broken: the same intended size came out
     * 0.66rem in one place and full size in the next.
     *
     * Derived from --font-size-text rather than fixed, so it follows the
     * reader's size setting the way everything else on this scale does. At
     * the default that is 0.717rem, which is where the hand-written fallbacks
     * already clustered.
     */
    --font-size-small: calc(var(--font-size-text) * 0.82);
    
    /* Font Weight Variables - 4 core weights */
    --font-weight-normal: 400;         /* Normal text weight */
    --font-weight-medium: 500;         /* Medium - asked for in nine places, defined in none */
    --font-weight-semibold: 600;       /* Semibold - labels, emphasis */
    --font-weight-bold: 700;           /* Bold - headings, important text */
    --font-weight-black: 900;          /* Extra bold - titles, strong emphasis */
}

/* Extra Small - xs (Level 0) */
body.font-size-xs {
    --font-size-title: 1.5rem;
    --font-size-date: 0.75rem;
    --font-size-category: 0.75rem;
    --font-size-bookmark: 0.813rem;
    --font-size-controls: 0.625rem;
    --font-size-text: 0.625rem;
}

/* Small - s (Level 1) */
body.font-size-s {
    --font-size-title: 1.75rem;
    --font-size-date: 0.813rem;
    --font-size-category: 0.813rem;
    --font-size-bookmark: 0.938rem;
    --font-size-controls: 0.688rem;
    --font-size-text: 0.688rem;
}

/* Small-Medium - sm (Level 2) */
body.font-size-sm {
    --font-size-title: 2rem;
    --font-size-date: 0.938rem;
    --font-size-category: 0.938rem;
    --font-size-bookmark: 1rem;
    --font-size-controls: 0.75rem;
    --font-size-text: 0.75rem;
}

/* Medium - m (Level 3 - Default) */
body.font-size-m {
    --font-size-title: 3rem;
    --font-size-date: 1.125rem;
    --font-size-category: 1.125rem;
    --font-size-bookmark: 1.25rem;
    --font-size-controls: 0.875rem;
    --font-size-text: 0.875rem;
}

/* Large-Medium - lg (Level 4) */
body.font-size-lg {
    --font-size-title: 3.5rem;
    --font-size-date: 1.25rem;
    --font-size-category: 1.25rem;
    --font-size-bookmark: 1.375rem;
    --font-size-controls: 0.938rem;
    --font-size-text: 0.938rem;
}

/* Large - l (Level 5) */
body.font-size-l {
    --font-size-title: 4rem;
    --font-size-date: 1.375rem;
    --font-size-category: 1.375rem;
    --font-size-bookmark: 1.5rem;
    --font-size-controls: 1rem;
    --font-size-text: 1rem;
}

/* Extra Large - xl (Level 6) */
body.font-size-xl {
    --font-size-title: 4.5rem;
    --font-size-date: 1.5rem;
    --font-size-category: 1.5rem;
    --font-size-bookmark: 1.625rem;
    --font-size-controls: 1.125rem;
    --font-size-text: 1.125rem;
}

/* Extra Large - xl (Level 6) */
body.font-size-xl {
    --font-size-title: 4.5rem;
    --font-size-date: 1.5rem;
    --font-size-category: 1.5rem;
    --font-size-bookmark: 1.625rem;
    --font-size-controls: 1.125rem;
    --font-size-text: 1.125rem;
}

/* Responsive adjustments for font sizes */
@media (max-width: 767px) {
    body.font-size-xl {
        --font-size-title: 3.5rem;
    }
    
    body.font-size-l {
        --font-size-title: 3rem;
    }
    
    body.font-size-lg {
        --font-size-title: 2.5rem;
    }
}

@media (max-width: 479px) {
    body.font-size-xl {
        --font-size-title: 3rem;
    }
    
    body.font-size-l {
        --font-size-title: 2.5rem;
    }
    
    body.font-size-lg {
        --font-size-title: 2.25rem;
    }
    
    body.font-size-m {
        --font-size-title: 2rem;
    }
    
    body.font-size-sm {
        --font-size-title: 1.75rem;
    }
    
    body.font-size-s {
        --font-size-title: 1.5rem;
    }
    
    body.font-size-xs {
        --font-size-title: 1.25rem;
    }
}

/* ==== css/responsive.css ==== */
/* Responsive Design System */

/* General mobile button styles */
.button-container {
    gap: var(--space-2, 0.5rem);
}

/* Large Desktop (1200px+) - Default styles already applied */

/* Desktop (992px - 1199px) */
@media (max-width: 1199px) {
    .dashboard-section.section-controls,
    .config-section.section-header {
        padding-top: 1.75rem;
    }
    
    .dashboard-section.section-content {
        padding-bottom: var(--space-7, 3rem); /* Space for search button */
    }
    
    .config-section.section-content {
        padding-top: var(--space-3, 0.875rem);
        padding-bottom: 1.75rem;
    }
    
    .container {
        /* Roomier clamp() margin (not a percentage) so the columns sit closer
           together, shrinking toward the floor as the window narrows. */
        max-width: 100%;
        padding: 0 clamp(var(--space-5, 1.5rem), 5vw, 6rem);
    }

    /* Keep the config page shell in step with the dashboard .container at this
       breakpoint so the config header/nav stay aligned with the dashboard. */
    html:has(#config-main) .config-page-shell {
        grid-template-columns: minmax(0, 1fr);
        max-width: 1600px;
        padding-inline: clamp(var(--space-5, 1.5rem), 5vw, 6rem);
    }

    /* Same for the health shell, so its header/nav stay aligned too. */
    .health-shell {
        max-width: 100%;
        padding-left: clamp(var(--space-5, 1.5rem), 5vw, 6rem);
        padding-right: clamp(var(--space-5, 1.5rem), 5vw, 6rem);
    }

    .dashboard-grid {
        gap: var(--space-6, 2rem);
    }
    
    .search-container {
        width: min(52rem, calc(100vw - 2.5rem));
        max-width: 100%;
    }
    
    /* No `bottom` here (or in the narrower breakpoints below): the button bar is
       positioned by .button-container, and these buttons are flex children of
       it. The declaration was a leftover from when each button was fixed
       individually. It was inert on the `static` ones, but .search-button
       [data-tooltip] sets position: relative for the tooltip anchor — so the
       one button the JS tooltip system does not manage (fold-all, which keeps
       its data-tooltip attribute) was lifted out of the row by exactly that
       many pixels as soon as a breakpoint applied. */
    .search-button {
        padding: var(--space-3, 0.8rem) var(--space-4, 1rem);
    }
    
    /* Tabs responsive - desktop */
    .tabs {
        flex-wrap: wrap;
    }
    
    /* List items responsive - desktop */
    .page-item,
    .category-item,
    .bookmark-item,
    .finder-item {
        flex-wrap: wrap;
        gap: var(--space-2, 0.5rem);
    }
    
    .form-group {
        flex-wrap: wrap;
    }
    
    .config-actions-top {
        flex-wrap: wrap;
        gap: var(--space-3, 0.75rem);
    }
}

/* Tablet (768px - 991px) */
@media (max-width: 991px) {
    .dashboard-section.section-controls,
    .config-section.section-header {
        padding-top: var(--space-5, 1.5rem);
    }
    
    .dashboard-section.section-content {
        padding-bottom: var(--space-7, 3rem); /* Space for search button */
    }
    
    .config-section.section-content {
        padding-top: var(--space-3, 0.75rem);
        padding-bottom: var(--space-5, 1.5rem);
    }
    
    .container {
        max-width: 95%;
        padding: 0 var(--space-3, 0.75rem);
    }
    
    /* The column count is not set here: the grid takes as many of its
       configured columns as fit (renderCore.getEffectiveColumnsPerRow). A
       fixed two here, and one below 768px, overrode that -- and the flex
       rule broke the packed masonry grid outright. */
    .dashboard-grid {
        gap: var(--space-5, 1.5rem);
    }
    
    .title {
        font-size: var(--font-size-title);
    }
    
    .header-actions {
        gap: var(--header-control-gap, 0.5rem);
        margin-left: auto;
    }
    
    .page-nav-btn {
        padding: var(--space-1-5, 0.35rem) var(--space-2, 0.5rem);
        font-size: var(--font-size-controls);
    }
    
    .config-link a {
        font-size: var(--font-size-controls);
    }
    
    .bookmark-link {
        padding: var(--space-1-5, 0.4rem) 0;
    }
    
    .config-button {
        padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
    }
    
    .search-container {
        width: min(52rem, calc(100vw - 2rem));
        max-width: 100%;
        padding: 1.25rem var(--space-5, 1.5rem) var(--space-3, 0.875rem) var(--space-6, 2rem);
    }
    
    .search-button {
        padding: var(--space-3, 0.8rem) var(--space-4, 1rem);
        font-size: var(--font-size-controls);
    }
    
    /* Config page tablet styles */
    .bookmark-item {
        display: grid;
        grid-template-columns: 1fr 1fr 100px 120px 80px;
        gap: var(--space-3, 0.75rem);
    }
    
    .finder-item {
        display: grid;
        grid-template-columns: 1fr 1fr 100px 80px;
        gap: var(--space-3, 0.75rem);
    }
    
    .category-item {
        display: grid;
        grid-template-columns: 1fr 80px;
    }
    
    .section {
        margin-bottom: var(--space-6, 2rem);
    }
    
    /* Colors page - stack vertically on tablet */
    .config-main,
    .custom-themes-main-grid {
        grid-template-columns: 1fr;
        gap: var(--space-6, 2rem);
    }
    
    /* Tabs responsive - tablet */
    .tabs {
        flex-wrap: wrap;
    }
    
    .tab-button {
        padding: var(--space-2, 0.625rem) 1.25rem;
        font-size: var(--font-size-controls);
    }
    
    .config-actions-top {
        flex-wrap: wrap;
        gap: var(--space-3, 0.75rem);
    }
}

/* Mobile Large (576px - 767px) */
@media (max-width: 767px) {
    .container {
        padding: var(--space-4, 1rem) var(--space-2, 0.5rem);
    }
    
    .dashboard-grid {
        gap: var(--space-5, 1.5rem);
    }
    
    .category h3 {
        margin-bottom: var(--space-3, 0.75rem);
    }
    
    .bookmark-link {
        padding: var(--space-1-5, 0.35rem) 0;
    }
    
    .config-button {
        padding: var(--space-2, 0.625rem) var(--space-3, 0.875rem);
    }
    
    .shortcut-hint {
        bottom: 1rem;
        right: 1rem;
        left: 1rem;
        text-align: center;
        padding: var(--space-2, 0.5rem);
    }
    
    .search-container {
        width: calc(100vw - 2rem);
        max-width: 100%;
        padding: var(--space-4, 1rem) 1.25rem var(--space-3, 0.75rem) var(--space-6, 2rem);
    }
    
    /* Prevent horizontal scroll on small screens */
    .search-prompt,
    .search-query,
    .search-match-name {
        max-width: 100%;
        overflow-x: hidden;
    }
    
    .search-match {
        padding: var(--space-1-5, 0.4rem) 0;
    }
    
    .search-button {
        padding: var(--space-3, 0.8rem) var(--space-4, 1rem);
        font-size: var(--font-size-controls);
    }
    
    .button-container {
        width: calc(100% - 2rem);
        justify-content: center;
        bottom: 1rem;
    }
    
    /* Header responsive - mobile large */
    .dashboard-section,
    .config-section {
        padding: 0;
    }
    
    .dashboard-section.section-controls,
    .config-section.section-header {
        padding-top: var(--space-5, 1.5rem);
    }

    .dashboard-section.section-controls {
        padding-bottom: 0;
    }

    .config-section.section-header {
        padding-bottom: 1.25rem;
    }
    
    .dashboard-section.section-title,
    .config-section.section-controls {
        padding-top: 0;
        padding-bottom: 0;
    }
    
    .dashboard-section.section-content {
        padding-top: 0;
        padding-bottom: var(--space-7, 3rem); /* Space for search button */
    }
    
    .config-section.section-content {
        padding-top: var(--space-3, 0.75rem);
        padding-bottom: var(--space-5, 1.5rem);
    }
    
    .container {
        padding: 0 var(--space-3, 0.75rem);
    }
    
    .header-top {
        flex-direction: row;
        align-items: center;
        gap: var(--space-2, 0.5rem);
    }

    .date {
        display: none;
    }

    .date-badge-mobile:not(:empty) {
        display: flex;
        align-items: center;
        font-size: calc(var(--font-size-controls) * 0.88);
        font-weight: var(--font-weight-semibold);
        color: var(--text-secondary);
        background: var(--background-secondary);
        border: 1px solid var(--border-primary);
        border-radius: 20px;
        padding: var(--space-hairline, 0.18rem) var(--space-2, 0.6rem);
        cursor: pointer;
        white-space: nowrap;
        user-select: none;
        transition: border-color 0.12s ease, color 0.12s ease;
        order: -1;
    }

    .date-badge-mobile:not(:empty):hover,
    .date-badge-mobile:not(:empty):focus-visible {
        border-color: var(--accent-primary);
        color: var(--text-primary);
        outline: none;
    }

    .title {
        font-size: var(--font-size-title);
    }

    .header-actions {
        flex-direction: row;
        align-items: center;
        gap: var(--space-3, 0.75rem);
        margin-left: auto;
        min-width: 0;
    }

    .header {
        flex-direction: column;
        align-items: center;
        gap: var(--space-3, 0.75rem);
    }

    .header .nav-links,
    .header .header-links {
        order: -1;
        display: flex;
        flex-direction: column;
        gap: var(--space-3, 0.75rem);
    }

    .header.config-header-top {
        flex-direction: row;
        align-items: flex-start;
        justify-content: space-between;
        gap: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
    }

    .header.config-header-top .header-links {
        order: 0;
        flex-direction: row;
        align-items: center;
        flex-wrap: wrap;
        gap: var(--space-3, 0.75rem) var(--space-4, 1rem);
    }

    .config-header-top .header-actions {
        margin-left: auto;
    }

    .page-navigation {
        gap: var(--space-hairline, 0.125rem);
        flex-wrap: nowrap;
        overflow-x: auto;
        scroll-snap-type: x mandatory;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
        min-width: 0;
    }

    .page-navigation::-webkit-scrollbar {
        display: none;
    }

    .page-nav-btn {
        scroll-snap-align: start;
        flex-shrink: 0;
    }

    .page-nav-btn {
        padding: var(--space-1, 0.3rem) var(--space-2, 0.45rem);
        font-size: var(--font-size-controls);
    }

    .page-swipe-hint {
        display: flex;
        align-items: center;
        font-size: calc(var(--font-size-controls) * 0.82);
        color: var(--text-tertiary);
        white-space: nowrap;
        flex-shrink: 0;
        opacity: 0;
        animation: swipeHintPulse 2.4s ease-in-out 0.6s 3 forwards;
        pointer-events: none;
        letter-spacing: 0.04em;
    }

    @keyframes swipeHintPulse {
        0%   { opacity: 0; }
        25%  { opacity: 0.7; }
        75%  { opacity: 0.7; }
        100% { opacity: 0; }
    }
    
    .config-link a {
        font-size: var(--font-size-controls);
    }
    
    /* Config page mobile large styles */
    .section {
        margin-bottom: var(--space-5, 1.5rem);
    }
    
    .page-selector-wrapper {
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-3, 0.75rem);
    }
    
    .page-selector-wrapper label {
        white-space: normal;
    }
    
    .page-selector {
        max-width: 100%;
        width: 100%;
    }
    
    .category-item,
    .bookmark-item,
    .finder-item {
        display: flex;
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-3, 0.75rem);
    }
    
    .category-item input,
    .bookmark-item input,
    .bookmark-item select,
    .finder-item input {
        width: 100%;
        margin-bottom: var(--space-2, 0.5rem);
    }
    
    .config-actions {
        flex-direction: column;
        gap: var(--space-3, 0.75rem);
    }
    
    .config-actions .btn {
        width: 100%;
    }
    
    .notification {
        bottom: max(1rem, env(safe-area-inset-bottom));
        right: max(1rem, env(safe-area-inset-right));
        left: auto;
        margin: 0;
        max-width: 320px;
        text-align: left;
    }
    
    /* Modal responsive */
    .modal {
        max-width: 320px;
        width: 95%;
        padding: var(--space-4, 1rem) 1.25rem;
    }
    
    .modal-body {
        margin-bottom: 1.25rem;
    }
    
    .modal-button {
        padding: var(--space-1-5, 0.375rem) var(--space-3, 0.75rem);
    }
    
    .modal-button-prefix {
        min-width: 2.5ch;
    }
    
    /* Colors page - stack vertically on mobile */
    .config-main,
    .custom-themes-main-grid {
        grid-template-columns: 1fr;
        gap: var(--space-6, 2rem);
    }
    
    .colors-grid {
        gap: var(--space-6, 2rem);
    }
    
    /* Tabs responsive - mobile large */
    .tabs {
        flex-wrap: wrap;
        gap: 0;
        top: 65px;
    }
    
    .tab-button {
        padding: var(--space-2, 0.5rem) var(--space-4, 1rem);
        font-size: var(--font-size-controls);
        flex: 1 1 auto;
        min-width: 120px;
    }
    
    .config-actions-top {
        flex-wrap: wrap;
        gap: var(--space-2, 0.625rem);
    }
    
    .config-actions-top .btn {
        flex: 1 1 auto;
        min-width: 140px;
    }
}

/* Mobile Small (480px - 575px) */
@media (max-width: 575px) {
    .container {
        padding: var(--space-3, 0.75rem) var(--space-1-5, 0.375rem);
    }
    
    .dashboard-grid {
        gap: 1.25rem;
    }
    
    .bookmark-link {
        padding: var(--space-1, 0.3rem) 0;
    }
    
    .config-button {
        padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
    }
    
    .search-container {
        width: calc(100% - 2rem);
        max-width: 100%;
        margin: 0 auto;
        padding: var(--space-3, 0.875rem) var(--space-4, 1rem) var(--space-2, 0.625rem) var(--space-6, 2rem);
    }
    
    .search-prompt {
        margin-bottom: var(--space-2, 0.5rem);
    }
    
    .search-button {
        padding: var(--space-3, 0.8rem) var(--space-4, 1rem);
        font-size: var(--font-size-text);
    }
    
    .button-container {
        width: calc(100% - 2rem);
        justify-content: center;
    }
    
    /* Header responsive - mobile small */
    .dashboard-section,
    .config-section {
        padding: 0;
    }
    
    .dashboard-section.section-controls,
    .config-section.section-header {
        padding-top: 1.25rem;
    }

    .dashboard-section.section-controls {
        padding-bottom: 0;
    }

    .config-section.section-header {
        padding-bottom: var(--space-4, 1.125rem);
    }
    
    .dashboard-section.section-title,
    .config-section.section-controls {
        padding-top: 0;
        padding-bottom: 0;
    }
    
    .dashboard-section.section-content {
        padding-top: 0;
        padding-bottom: var(--space-7, 3rem); /* Space for search button */
    }
    
    .config-section.section-content {
        padding-top: var(--space-2, 0.625rem);
        padding-bottom: 1.25rem;
    }
    
    .container {
        padding: 0 var(--space-2, 0.5rem);
    }
    
    .header-top {
        flex-direction: row;
        align-items: center;
        gap: var(--space-2, 0.5rem);
    }

    .title {
        font-size: var(--font-size-title);
    }

    .header-actions {
        gap: var(--space-2, 0.625rem);
        margin-left: auto;
    }
    
    .header {
        flex-direction: column;
        align-items: center;
        gap: var(--space-2, 0.625rem);
    }
    
    .header .nav-links,
    .header .header-links {
        order: -1;
        display: flex;
        flex-direction: column;
        gap: var(--space-2, 0.625rem);
    }

    .header.config-header-top {
        flex-direction: row;
        align-items: flex-start;
        justify-content: space-between;
        gap: var(--space-2, 0.5rem) var(--space-2, 0.625rem);
    }

    .header.config-header-top .header-links {
        order: 0;
        flex-direction: row;
        align-items: center;
        flex-wrap: wrap;
        gap: var(--space-2, 0.625rem) var(--space-3, 0.85rem);
    }

    .config-header-top .header-actions {
        margin-left: auto;
    }
    
    .page-nav-btn {
        padding: var(--space-1, 0.25rem) var(--space-1-5, 0.4rem);
        font-size: var(--font-size-controls);
    }

    .config-link a {
        font-size: var(--font-size-text);
    }
    
    /* Config page mobile small styles */
    .section {
        margin-bottom: 1.25rem;
    }
    
    .form-group {
        margin-bottom: var(--space-4, 1rem);
    }
    
    .btn {
        padding: var(--space-2, 0.625rem) var(--space-4, 1rem);
    }
    
    /* Tabs responsive - mobile small */
    .tabs {
        top: auto;
        position: relative;
    }
    
    .tab-button {
        padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
        font-size: var(--font-size-controls);
        flex: 1 1 auto;
        min-width: 100px;
    }
    
    .config-actions-top {
        flex-direction: column;
    }
    
    .config-actions-top .btn {
        width: 100%;
    }
}

/* Mobile Extra Small (up to 479px) */
@media (max-width: 479px) {
    .container {
        padding: var(--space-2, 0.5rem) var(--space-1, 0.25rem);
    }
    
    .dashboard-grid {
        /* Deliberately no gap on the narrowest screens, so this is a plain 0
           rather than a token: the spacing scale has no zero step, and
           --space-hairline turned it into 2.4px. */
        gap: 0;
    }
    
    .category {
        margin-bottom: var(--space-5, 1.5rem);
    }
    
    .category h3 {
        margin-bottom: var(--space-2, 0.5rem);
    }
    
    .bookmark-link {
        padding: var(--space-1, 0.25rem) 0;
    }
    
    .config-button {
        top: 0.5rem;
        right: 0.5rem;
        padding: var(--space-1-5, 0.4rem) var(--space-2, 0.625rem);
    }
    
    .shortcut-hint {
        padding: var(--space-1-5, 0.375rem);
        bottom: 0.5rem;
        right: 0.5rem;
        left: 0.5rem;
    }
    
    .search-container {
        width: calc(100% - 2rem);
        max-width: 100%;
        margin: 0 auto;
        padding: var(--space-3, 0.75rem) var(--space-3, 0.875rem) var(--space-2, 0.5rem) var(--space-6, 2rem);
    }
    
    .search-prompt {
        margin-bottom: var(--space-2, 0.5rem);
    }
    
    .search-match {
        padding: var(--space-1, 0.3rem) 0;
    }
    
    .search-match-shortcut {
        min-width: 2.5ch;
    }
    
    .search-button {
        padding: var(--space-3, 0.8rem) var(--space-4, 1rem);
        font-size: var(--font-size-controls);
    }
    
    .button-container {
        width: calc(100% - 2rem);
        justify-content: center;
    }
    
    .search-button-icon {
        font-size: var(--font-size-controls);
    }
    
    /* Header responsive - mobile extra small */
    .dashboard-section,
    .config-section {
        padding: 0;
    }
    
    .dashboard-section.section-controls,
    .config-section.section-header {
        padding-top: var(--space-4, 1rem);
    }

    .dashboard-section.section-controls {
        padding-bottom: 0;
    }

    .config-section.section-header {
        padding-bottom: var(--space-4, 1rem);
    }
    
    .dashboard-section.section-title,
    .config-section.section-controls {
        padding-top: 0;
        padding-bottom: 0;
    }
    
    .dashboard-section.section-content {
        padding-top: 0;
        padding-bottom: var(--space-7, 3rem); /* Space for search button */
    }
    
    .config-section.section-content {
        padding-top: var(--space-2, 0.5rem);
        padding-bottom: var(--space-4, 1rem);
    }
    
    .container {
        padding: 0 var(--space-2, 0.5rem);
    }
    
    .header-top {
        flex-direction: row;
        align-items: center;
        gap: var(--space-2, 0.5rem);
    }

    .title {
        font-size: var(--font-size-title);
    }

    .header-actions {
        gap: var(--space-2, 0.5rem);
        margin-left: auto;
    }
    
    .header {
        flex-direction: column;
        align-items: center;
        gap: var(--space-2, 0.5rem);
    }
    
    .header .nav-links,
    .header .header-links {
        order: -1;
        display: flex;
        flex-direction: column;
        gap: var(--space-2, 0.5rem);
    }

    .header.config-header-top {
        flex-direction: row;
        align-items: flex-start;
        justify-content: space-between;
        gap: var(--space-2, 0.5rem);
    }

    .header.config-header-top .header-links {
        order: 0;
        flex-direction: row;
        align-items: center;
        flex-wrap: wrap;
        gap: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
    }

    .config-header-top .header-actions {
        margin-left: auto;
    }
    
    .page-navigation {
        flex-wrap: nowrap;
        overflow-x: auto;
        gap: var(--space-hairline, 0.125rem);
        justify-content: flex-start;
    }

    .page-nav-btn {
        padding: var(--space-1, 0.25rem) var(--space-1-5, 0.35rem);
        font-size: var(--font-size-controls);
        white-space: nowrap;
    }

    .config-link a {
        font-size: var(--font-size-controls);
        white-space: nowrap;
    }
    
    /* Config page mobile extra small styles */
    .section {
        margin-bottom: var(--space-4, 1rem);
    }
    
    .section h2 {
        margin-bottom: var(--space-3, 0.75rem);
    }
    
    .form-group {
        margin-bottom: var(--space-3, 0.875rem);
    }
    
    .form-control {
        padding: var(--space-2, 0.5rem);
    }
    
    .btn {
        padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
    }
    
    .config-actions {
        gap: var(--space-2, 0.5rem);
    }
    
    .notification {
        bottom: max(0.5rem, env(safe-area-inset-bottom));
        right: max(0.5rem, env(safe-area-inset-right));
        left: auto;
        margin: 0;
        max-width: 280px;
        padding: var(--space-3, 0.75rem);
        text-align: left;
    }
    
    .category-item,
    .bookmark-item,
    .finder-item {
        gap: var(--space-2, 0.5rem);
    }
    
    .category-item input,
    .bookmark-item input,
    .bookmark-item select,
    .finder-item input {
        margin-bottom: var(--space-1-5, 0.375rem);
    }
    
    /* Modal extra small */
    .modal {
        max-width: 280px;
        width: 90%;
        padding: var(--space-3, 0.875rem) var(--space-4, 1rem);
    }
    
    .modal-body {
        margin-bottom: var(--space-4, 1rem);
    }
    
    .modal-actions {
        gap: var(--space-3, 0.75rem);
    }
    
    .modal-button {
        padding: var(--space-1, 0.3rem) var(--space-2, 0.5rem);
    }
    
    .modal-button-prefix {
        min-width: 2ch;
        margin-right: var(--space-3, 0.75rem);
    }
    
    /* Tabs responsive - mobile extra small */
    .tabs {
        position: relative;
        top: auto;
        flex-direction: column;
    }
    
    .tab-button {
        padding: var(--space-2, 0.5rem);
        font-size: var(--font-size-controls);
        text-align: center;
        width: 100%;
        border-bottom: 1px solid var(--border-primary);
    }
    
    .tab-button:last-child {
        border-bottom: none;
    }

    /* Fix for theme and language selectors in config general tab */
    .form-group:has(#theme-select),
    .form-group:has(#language-select) {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--space-2, 0.5rem);
    }

    .form-group:has(#theme-select) label,
    .form-group:has(#language-select) label {
        min-width: auto;
    }

    .form-group--theme-select .custom-select-wrapper,
    .form-group--theme-select select.config-select-wide {
        min-width: 100%;
        max-width: 100%;
        flex: 1 1 100%;
    }
}

@media (prefers-reduced-motion: reduce) {
    /* Swipe hint → show statically, no pulse animation */
    .page-swipe-hint { animation: none !important; opacity: 0.55 !important; }
}

/* ==== css/dashboard-enhancements.css ==== */
/* Dashboard discoverability: header, mobile bar, mini status, a11y, kbd tooltips */

:root {
    --header-chip-radius: 999px;
    --header-chip-pad-block: 0.38rem;
    --header-chip-pad-inline: 0.8rem;
    --layout-radius-xs: 4px;
    --layout-radius-sm: 8px;
    --layout-radius-md: 12px;
    --dashboard-toolbar-stack: 5.75rem;
}

kbd,
.keyboard-cheat-sheet-keys kbd {
    display: inline-block;
    padding: 0.05em 0.35em;
    border: 1px solid color-mix(in srgb, var(--text-secondary) 40%, transparent);
    border-radius: 0.2em;
    background: color-mix(in srgb, var(--background-secondary) 90%, transparent);
    font-family: var(--font-family-main, "Source Code Pro", monospace);
    font-size: 0.92em;
    font-weight: var(--font-weight-bold);
    line-height: 1.3;
    color: var(--text-primary);
}

.kbd-sep,
.kbd-or {
    margin: 0 0.2em;
    color: var(--text-secondary);
    font-size: 0.85em;
    font-weight: var(--font-weight-normal);
}

.toolbar-kbd-tooltip {
    position: fixed;
    z-index: 10050;
    /* --kbd-tooltip-shift nudges the box back inside the window when centring on
       the anchor would push it past the left or right edge. */
    --kbd-tooltip-shift: 0px;
    transform: translate(calc(-50% + var(--kbd-tooltip-shift)), calc(-100% - 8px));
    display: none;
    flex-direction: column;
    align-items: center;
    gap: var(--space-1, 0.2rem);
    padding: var(--space-1-5, 0.35rem) var(--space-2, 0.55rem);
    background: var(--background-secondary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-3, 0.3rem);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
    pointer-events: none;
    white-space: nowrap;
    visibility: hidden;
    opacity: 0;
}

.toolbar-kbd-tooltip.is-visible {
    display: flex;
    visibility: visible;
    opacity: 1;
}

/* Header icons: tooltip appears BELOW the button, because there is no room
   above them at the top of the window. */
.toolbar-kbd-tooltip--below {
    transform: translate(calc(-50% + var(--kbd-tooltip-shift)), 8px);
}

.toolbar-kbd-tooltip-label {
    font-size: calc(var(--font-size-controls) * 0.82);
    color: var(--text-secondary);
    text-transform: lowercase;
}

.toolbar-kbd-tooltip-keys {
    display: inline-flex;
    align-items: center;
    gap: var(--space-hairline, 0.15rem);
    flex-wrap: wrap;
    justify-content: center;
}

/*
 * These chips name the same shortcuts as the toolbar buttons at the bottom of
 * the screen, so they carry that button's fill. The bare `kbd` rule in
 * enhanced-features.css loads later and would otherwise win with
 * --background-primary, which reads as a different, denser block here.
 */
#toolbar-kbd-tooltip .toolbar-kbd-tooltip-keys kbd {
    background: color-mix(in srgb, var(--background-secondary) 74%, transparent);
    border-color: var(--border-primary);
}

/* Dashboard discoverability: header, mobile bar, mini status, a11y */

.skip-to-content {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: 10000;
    padding: var(--space-2, 0.5rem) var(--space-4, 1rem);
    background: var(--bg-primary, #000);
    color: var(--text-primary, #fff);
    border: 1px solid var(--text-primary);
    font-family: var(--font-family-main);
    text-decoration: none;
}

.skip-to-content:focus-visible {
    left: 0.5rem;
    top: 0.5rem;
}


/* Dashboard discoverability: header, mobile bar, mini status */

.header-top {
    /*
     * One row down to the phone breakpoint. It wrapped, so a track that grew
     * carried the toolbar onto a second line; the track is bounded now and
     * shrinks instead (see .page-navigation in dashboard.css).
     */
    flex-wrap: nowrap;
    gap: var(--space-3, 0.75rem) var(--space-4, 1rem);
}

.header-top-primary {
    display: flex;
    flex-direction: column;
    gap: var(--space-1-5, 0.35rem);
    flex: 1 1 12rem;
    min-width: 0;
}

.dashboard-mini-status {
    display: none;
    font-size: calc(var(--font-size-controls) * 0.95);
    color: var(--text-tertiary);
    font-family: var(--font-family-main);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}


body .dashboard-mini-status,
body .status-loading-indicator.show,
body .date-badge-mobile:not(:empty),
body .page-swipe-hint {
    padding: var(--header-chip-pad-block, 0.38rem) var(--header-chip-pad-inline, 0.8rem);
    border-radius: var(--header-chip-radius, 999px);
    border: 1px solid color-mix(in srgb, var(--text-secondary) 35%, transparent);
    background: color-mix(in srgb, var(--background-secondary) 55%, transparent);
}

/* The pages button is one of the header actions (.search-button in
   dashboard.css); inbox, health and config share the destination chrome. */


.search-button-label {
    display: none;
    font-size: var(--text-2xs, 0.58rem);
    line-height: 1;
    text-transform: lowercase;
    color: var(--text-secondary);
    font-family: var(--font-family-main);
}

.search-flow-hint .sfh-seg-swipe.hidden {
    display: none;
}

@media (max-width: 768px), (hover: none) and (pointer: coarse) {
    .dashboard-mini-status {
        display: block;
    }

    .search-flow-hint .sfh-seg-swipe.hidden {
        display: inline;
    }

    .search-button[data-tooltip]::after {
        display: none !important;
    }
}

@media (max-width: 575px) {
    .date {
        display: none;
    }
}

.save-status-indicator.is-saved-flash {
    animation: config-saved-flash 1.4s ease;
}


@keyframes first-bookmark-pulse {
    0%, 100% { box-shadow: 0 0 0 0 transparent; }
    50% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-success, #00ff9c) 55%, transparent); }
}

@keyframes config-saved-flash {
    0% { opacity: 0.5; transform: scale(0.96); }
    30% { opacity: 1; transform: scale(1.02); color: var(--accent-success, #00ff9c); }
    100% { opacity: 1; transform: scale(1); }
}

@media (max-width: 768px), (hover: none) and (pointer: coarse) {
    :root {
        --dashboard-toolbar-stack: 6.75rem;
    }
}

:is(
    .dashboard-search-promo,
    .dashboard-grid-kbd-promo,
    .dashboard-feature-promo
) kbd {
    border-radius: 0.2em;
}

/* ==== css/skeleton-loading.css ==== */
/* Shared skeleton placeholders while body.loading */

body.loading {
    overflow-x: hidden;
}

.skeleton-root {
    display: none;
    pointer-events: none;
    user-select: none;
}

body.loading .skeleton-root {
    display: block;
}

body.loading .skeleton-dashboard.skeleton-root,
body.loading .skeleton-colors-grid.skeleton-root {
    display: grid;
}

body.loading .skeleton-hide-while-loading {
    visibility: hidden !important;
}

body.loading #config-main > :not([data-skeleton-root]),
body.loading .colors-main > :not([data-skeleton-root]) {
    visibility: hidden;
}

.skeleton-shimmer {
    background: linear-gradient(
        90deg,
        color-mix(in srgb, var(--text-secondary, #888) 12%, transparent) 0%,
        color-mix(in srgb, var(--text-secondary, #888) 22%, transparent) 50%,
        color-mix(in srgb, var(--text-secondary, #888) 12%, transparent) 100%
    );
    background-size: 200% 100%;
    animation: skeleton-shimmer 0.75s ease-in-out infinite;
    border-radius: var(--radius-1, 2px);
}

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

.skeleton-line {
    height: 0.85rem;
    margin-bottom: var(--space-2, 0.5rem);
}

.skeleton-line--sm {
    height: 0.65rem;
    width: 40%;
}

.skeleton-line--title {
    height: 1.1rem;
    width: 55%;
    margin-bottom: var(--space-3, 0.75rem);
}

.skeleton-line--wide {
    width: 85%;
}

.skeleton-line--pill {
    height: 1.5rem;
    width: 4.5rem;
    border-radius: var(--radius-pill, 999px);
    display: inline-block;
    margin-right: var(--space-2, 0.5rem);
}

/* Dashboard */
.skeleton-dashboard {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: var(--space-5, 1.5rem);
    width: 100%;
    padding: var(--space-1, 0.25rem) 0;
}

.skeleton-dashboard-col {
    display: flex;
    flex-direction: column;
    gap: var(--space-1-5, 0.35rem);
}

.skeleton-bookmark-row {
    height: 1.35rem;
    width: 100%;
}

.skeleton-bookmark-row--short {
    width: 72%;
}

/* Config */
.skeleton-config-panel {
    padding: var(--space-2, 0.5rem) 0 var(--space-6, 2rem);
}

.skeleton-config-cards {
    display: grid;
    gap: var(--space-4, 1rem);
    max-width: 720px;
}

.skeleton-config-card {
    border: 1px solid color-mix(in srgb, var(--text-secondary, #888) 25%, transparent);
    padding: var(--space-4, 1rem);
    min-height: 5rem;
}

/* Colors */
.skeleton-colors-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: var(--space-4, 1rem);
}

.skeleton-colors-group {
    min-height: 8rem;
    border: 1px solid color-mix(in srgb, var(--text-secondary, #888) 25%, transparent);
    padding: var(--space-3, 0.75rem);
}

.skeleton-header-pills {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1-5, 0.35rem);
    margin-top: var(--space-1-5, 0.35rem);
}

@media (prefers-reduced-motion: reduce) {
    .skeleton-shimmer {
        animation: none;
        background: color-mix(in srgb, var(--text-secondary, #888) 18%, transparent);
    }
}

body.no-animations .skeleton-shimmer {
    animation: none;
    background: color-mix(in srgb, var(--text-secondary, #888) 18%, transparent);
}

/* ==== css/dashboard-quickstart.css ==== */
/*
 * Quick-start card: the first-run checklist.
 * Non-blocking, bottom-left, theme-driven. Mirrors feature-spotlight conventions
 * so it reads as part of the same system.
 */

/*
 * At the bottom edge.
 *
 * The card used to be lifted two rows, to keep it off the tag cloud's and What's
 * New's buttons in this corner. Neither stands here any more -- the tag cloud's
 * is one of the action buttons, and What's New moved to the right -- so the
 * lift only left every invitation floating above the edge.
 */
.quickstart-card {
    position: fixed;
    left: max(1rem, env(safe-area-inset-left));
    bottom: max(1rem, env(safe-area-inset-bottom));
    width: 340px;
    max-width: calc(100vw - 2rem);
    background: var(--background-secondary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-5, 0.5rem);
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.18);
    z-index: 2147482800;
    opacity: 0;
    visibility: hidden;
    transform: translateY(12px);
    transition: opacity 0.26s ease, visibility 0.26s ease, transform 0.26s ease;
    overflow: hidden;
    font-family: var(--font-family-main, inherit);
}

.quickstart-card.show {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

.quickstart-stripe {
    height: 3px;
    background: var(--accent-primary);
}

.quickstart-inner {
    padding: var(--space-3, 0.875rem) var(--space-4, 1rem) var(--space-4, 0.9rem);
}

.quickstart-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2, 0.5rem);
}

.quickstart-title {
    font-size: var(--font-size-controls, 0.82rem);
    font-weight: 600;
    color: var(--text-primary);
    margin: 0 0 var(--space-1-5, 0.35rem);
    line-height: 1.3;
}

.quickstart-head .quickstart-title {
    margin-bottom: 0;
}

.quickstart-close {
    flex-shrink: 0;
    background: transparent;
    border: none;
    color: var(--text-secondary);
    font-size: var(--text-lg, 1.15rem);
    line-height: 1;
    cursor: pointer;
    padding: 0 var(--space-1, 0.2rem);
    border-radius: var(--radius-3, 0.25rem);
}

.quickstart-close:hover,
.quickstart-close:focus-visible {
    color: var(--text-primary);
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
}

/* ---- Checklist ---- */

.quickstart-progress {
    font-size: var(--font-size-small, 0.7rem);
    color: var(--text-secondary);
    margin: var(--space-1-5, 0.4rem) 0 var(--space-2, 0.55rem);
    letter-spacing: 0.02em;
}

.quickstart-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-1-5, 0.4rem);
}

.quickstart-item {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2, 0.55rem);
}

/* Actionable rows: the button becomes the flex row so the layout matches
   a plain item exactly. */
.quickstart-item.is-actionable {
    display: block;
}

.quickstart-item-action {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2, 0.55rem);
    width: 100%;
    margin: -0.15rem -0.3rem;
    padding: var(--space-hairline, 0.15rem) var(--space-1, 0.3rem);
    border: 0;
    border-radius: var(--radius-3, 5px);
    background: none;
    font-family: inherit;
    text-align: left;
    cursor: pointer;
}

.quickstart-item-action:hover,
.quickstart-item-action:focus-visible {
    background: color-mix(in srgb, var(--accent-primary) 10%, transparent);
    outline: none;
}

.quickstart-item-action:focus-visible {
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-primary) 35%, transparent);
}

/* Underline only while not done — .is-done owns text-decoration for the
   strikethrough, and the shorthand would otherwise cancel it. */
.quickstart-item.is-actionable:not(.is-done) .quickstart-item-label {
    text-decoration: underline;
    text-decoration-color: color-mix(in srgb, var(--accent-primary) 45%, transparent);
    text-underline-offset: 2px;
}

.quickstart-check {
    flex-shrink: 0;
    width: 18px;
    height: 18px;
    margin-top: 1px;
    border-radius: var(--radius-pill, 999px);
    border: 1.5px solid var(--border-primary);
    display: flex;
    align-items: center;
    justify-content: center;
    /* The tick sits on the accent once done: page ink, like the primary button. */
    color: var(--background-primary);
    transition: background 0.2s ease, border-color 0.2s ease;
}

.quickstart-item.is-done .quickstart-check {
    background: var(--accent-primary);
    border-color: var(--accent-primary);
}

.quickstart-check svg {
    width: 11px;
    height: 11px;
    stroke: currentColor;
    fill: none;
    stroke-width: 3;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.quickstart-item-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.quickstart-item-label {
    font-size: var(--font-size-small, 0.74rem);
    color: var(--text-primary);
    line-height: 1.35;
}

.quickstart-item.is-done .quickstart-item-label {
    color: var(--text-secondary);
    text-decoration: line-through;
    text-decoration-color: color-mix(in srgb, var(--text-secondary) 55%, transparent);
}

.quickstart-item-hint {
    font-size: var(--font-size-small, 0.66rem);
    color: var(--text-tertiary);
    line-height: 1.35;
    margin-top: var(--space-hairline, 0.05rem);
}

.quickstart-card.is-complete .quickstart-stripe {
    background: var(--accent-success);
}

/* [hidden] must win over the flex display above (e.g. weather location field). */
.quickstart-card [hidden] {
    display: none !important;
}

@media (prefers-reduced-motion: reduce) {
    .quickstart-card {
        transition: opacity 0.01s linear;
        transform: none;
    }
}

/* On small screens, span the width so it doesn't crowd the corner. */
@media (max-width: 560px) {
    .quickstart-card {
        left: 0.75rem;
        right: 0.75rem;
        width: auto;
        max-width: none;
    }
}

/* ── Notice cards ───────────────────────────────────────────────────────
   The bottom-left invitations (analytics, theme browser, and any future one) all
   reuse .quickstart-card for placement and chrome; these are the shared parts
   of the body. Built by NoticeCard.define — see static/js/notice-card.js.
   Previously duplicated per card, which is how they drifted apart. */
.notice-card-text {
    margin: var(--space-1-5, 0.35rem) 0 0;
    font-size: var(--font-size-small, 0.72rem);
    color: var(--text-secondary);
    line-height: 1.5;
}

/* A footnote rather than part of the pitch: quieter than the body text so it
   reads as a caveat rather than a second paragraph. */
.notice-card-caveat {
    margin: var(--space-2, 0.5rem) 0 0;
    padding-left: var(--space-2, 0.55rem);
    border-left: 2px solid var(--border-color, rgba(128, 128, 128, 0.35));
    font-size: var(--font-size-small, 0.72rem);
    color: var(--text-muted, var(--text-secondary));
    line-height: 1.45;
    opacity: 0.9;
}

/* Why an action failed, kept on the card rather than in a toast that fades
   before it can be read. Styled like the caveat, in the error colour. */
.notice-card-error {
    margin: var(--space-2, 0.5rem) 0 0;
    padding-left: var(--space-2, 0.55rem);
    border-left: 2px solid var(--accent-error, #c0392b);
    font-size: var(--font-size-small, 0.72rem);
    color: var(--accent-error, #c0392b);
    line-height: 1.45;
}

.notice-card-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2, 0.5rem);
    margin-top: var(--space-3, 0.8rem);
}

/* The buttons every NoticeCard draws (and the analytics and theme-browser
   notices, which build theirs by hand). They outlived the first-run setup
   window they were written for; removing them with it left every corner card
   with the browser's own grey buttons. */
.quickstart-btn {
    font-family: inherit;
    font-size: var(--font-size-small, 0.74rem);
    padding: var(--space-1-5, 0.4rem) var(--space-3, 0.85rem);
    border-radius: var(--radius-4, 0.35rem);
    border: 1px solid var(--border-primary);
    cursor: pointer;
    background: var(--background-primary);
    color: var(--text-primary);
}

.quickstart-btn-ghost {
    background: transparent;
}

/* A third answer that is not a third peer.
   A long label ("Remind me in 30 days", and longer once translated) wraps onto
   its own row, where a full-width bordered button reads as the heaviest choice
   on the card -- the opposite of what a postponement should look like. As plain
   text it can take the whole row without claiming it. */
.quickstart-btn-quiet {
    background: transparent;
    border-color: transparent;
    padding-left: 0;
    padding-right: 0;
    color: var(--text-muted, var(--text-secondary));
    text-decoration: underline;
    text-underline-offset: 2px;
}

.quickstart-btn-quiet:hover,
.quickstart-btn-quiet:focus-visible {
    color: var(--text-primary);
}

/* Ink from the page, not white: on a light accent (brass, amber, mint) white
   fell below 2:1, while the page colour sits at the far end from the accent
   on dark and light themes alike -- as every other filled accent button draws. */
.quickstart-btn-primary {
    background: var(--accent-primary);
    border-color: var(--accent-primary);
    color: var(--background-primary);
    font-weight: 600;
}

.quickstart-btn:hover,
.quickstart-btn:focus-visible {
    filter: brightness(1.05);
}

/*
 * The clock-and-weather card's three answers, on the card itself.
 *
 * A label above its control rather than beside it: the card is 340px wide and
 * a two-column row left "Temperature" wrapping over two lines beside a pair of
 * buttons. The choices are buttons and not a select, because two options are
 * cheaper to read than to open.
 */
.clock-weather-fields {
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 0.55rem);
    margin-top: var(--space-3, 0.7rem);
}

.clock-weather-row {
    display: flex;
    flex-direction: column;
    gap: var(--space-1, 0.25rem);
}

.clock-weather-label {
    font-size: var(--font-size-small, 0.7rem);
    color: var(--text-tertiary);
}

.clock-weather-input {
    width: 100%;
    padding: var(--space-1-5, 0.4rem) var(--space-2, 0.5rem);
    font-family: inherit;
    font-size: var(--font-size-small, 0.74rem);
    color: var(--text-primary);
    background: var(--background-primary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-4, 0.35rem);
}

.clock-weather-choices {
    display: flex;
    gap: var(--space-1-5, 0.35rem);
    flex-wrap: wrap;
}

.clock-weather-choice {
    padding: var(--space-1, 0.28rem) var(--space-2, 0.6rem);
    font-family: inherit;
    font-size: var(--font-size-small, 0.74rem);
    color: var(--text-secondary);
    background: var(--background-primary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-4, 0.35rem);
    cursor: pointer;
}

.clock-weather-choice[aria-pressed="true"] {
    color: var(--text-primary);
    border-color: var(--accent-primary);
    background: color-mix(in srgb, var(--accent-primary) 14%, transparent);
}

/* Shown only after the invitation has been taken up: the command that does the
   same thing again, for the one person who has just proved they care about it. */
.theme-browser-notice-hint {
    margin: var(--space-2, 0.6rem) 0 0;
    width: 100%;
    font-size: var(--font-size-small, 0.72rem);
    color: var(--text-tertiary);
    line-height: 1.5;
}

.theme-browser-notice-hint kbd {
    font-family: var(--font-family-main);
    font-size: var(--text-2xs, 0.68rem);
    background: var(--background-secondary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-2, 3px);
    padding: var(--space-hairline, 0.05rem) var(--space-1, 0.3rem);
    color: var(--text-secondary);
}

/* The shared .whats-new-modal shell only scrolls once it hits max-height; this
   modal is shorter than that, so cap the body itself or the last paragraph runs
   under the buttons. */
.analytics-notice-modal .modal-body {
    /* Roomy enough that the three sections normally fit whole; scrolls only on
       short viewports rather than clipping a line mid-sentence. */
    max-height: min(72vh, 40rem);
    overflow-y: auto;
    /* Overlay-style scrollbars hide until you scroll, so the cut-off text reads
       as clipped rather than scrollable. Keep the track visible instead. */
    scrollbar-width: thin;
    scrollbar-gutter: stable;
}

.analytics-notice-modal .modal-body::-webkit-scrollbar {
    width: 8px;
}

.analytics-notice-modal .modal-body::-webkit-scrollbar-thumb {
    background: var(--border-primary);
    border-radius: var(--radius-3, 4px);
}

/* Modal body: same rhythm as the what's-new release sections. */
.analytics-notice-modal-body p {
    margin: 0 0 var(--space-4, 1rem);
    line-height: 1.6;
    color: var(--text-secondary);
}

.analytics-notice-modal-body p:last-child {
    margin-bottom: 0;
}

.analytics-notice-heading {
    margin: 0 0 var(--space-1-5, 0.35rem);
    font-size: var(--font-size-text, 0.85rem);
    font-weight: var(--font-weight-semibold, 600);
    color: var(--text-primary);
}

.analytics-notice-modal-body .analytics-notice-heading:not(:first-child) {
    margin-top: 1.25rem;
}/* The stripe spans the full width at the very top of the card, so a rounded
   card clips its upper corners. Round its own top edge to match. */

@media (prefers-reduced-motion: reduce) {
}


/*
 * First-start card: "What do you run?"
 *
 * In the page, not in a corner: it stands above main's blocks, full width of
 * the grid, and the page under it keeps working. Four tiles closed, one
 * address with the services under More open.
 */
.first-start-card {
    margin: 0 0 var(--space-4, 1rem);
    background: var(--background-secondary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-5, 0.5rem);
    overflow: hidden;
    font-family: var(--font-family-main, inherit);
    color: var(--text-primary);
}

.first-start-card[hidden] {
    display: none;
}

.first-start-stripe {
    height: 3px;
    background: var(--accent-primary);
}

.first-start-inner {
    padding: var(--space-3, 0.875rem) var(--space-4, 1rem) var(--space-4, 1rem);
}

.first-start-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2, 0.5rem);
}

.first-start-title {
    margin: 0;
    font-size: var(--font-size-controls, 0.85rem);
    font-weight: 600;
}

.first-start-sub,
.first-start-note {
    margin: var(--space-1, 0.25rem) 0 var(--space-3, 0.75rem);
    font-size: var(--font-size-small, 0.8rem);
    color: var(--text-secondary);
    line-height: 1.45;
}

.first-start-note {
    margin: var(--space-2, 0.5rem) 0 0;
}

.first-start-close {
    background: transparent;
    border: none;
    color: var(--text-secondary);
    font-size: var(--text-lg, 1.15rem);
    line-height: 1;
    cursor: pointer;
    padding: 0 var(--space-1, 0.2rem);
    border-radius: var(--radius-3, 0.25rem);
}

.first-start-close:hover,
.first-start-close:focus-visible {
    color: var(--text-primary);
}

.first-start-tiles {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--space-2, 0.5rem);
}

@media (max-width: 720px) {
    .first-start-tiles {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

.first-start-tile {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-1, 0.25rem);
    min-width: 0;
    padding: var(--space-2-5, 0.65rem) var(--space-3, 0.75rem);
    background: var(--background-primary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-4, 0.375rem);
    color: var(--text-primary);
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.first-start-tile:hover,
.first-start-tile:focus-visible {
    border-color: var(--accent-primary);
}

.first-start-tile-name {
    font-size: var(--font-size-controls, 0.85rem);
}

.first-start-tile-icon {
    color: var(--accent-primary);
}

.first-start-tile-services {
    font-size: var(--font-size-small, 0.78rem);
    color: var(--text-secondary);
    line-height: 1.45;
    overflow-wrap: anywhere;
}

.first-start-tile-count {
    margin-top: auto;
    font-size: var(--font-size-small, 0.78rem);
    color: var(--accent-primary);
}

.first-start-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
    font-size: var(--font-size-controls, 0.85rem);
}

.first-start-row .first-start-input {
    flex: 1 1 14rem;
}

.first-start-input {
    min-width: 0;
    padding: var(--space-1-5, 0.35rem) var(--space-2, 0.5rem);
    background: var(--background-primary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-3, 0.25rem);
    color: var(--text-primary);
    font: inherit;
    font-size: var(--font-size-controls, 0.85rem);
}

.first-start-input:focus-visible {
    outline: none;
    border-color: var(--accent-primary);
}

.first-start-more,
.first-start-back {
    background: transparent;
    border: none;
    padding: 0;
    color: var(--accent-primary);
    font: inherit;
    cursor: pointer;
}

.first-start-back {
    color: var(--text-secondary);
}

.first-start-svcs {
    display: grid;
    grid-template-columns: minmax(6rem, 10rem) minmax(0, 1fr);
    gap: var(--space-1-5, 0.35rem) var(--space-3, 0.75rem);
    align-items: center;
    margin: var(--space-3, 0.75rem) 0 0;
    font-size: var(--font-size-small, 0.8rem);
}

.first-start-svcs[hidden] {
    display: none;
}

.first-start-svc-name {
    color: var(--text-secondary);
    overflow-wrap: anywhere;
}

.first-start-error {
    margin: var(--space-2, 0.5rem) 0 0;
    font-size: var(--font-size-small, 0.8rem);
    color: var(--accent-error, var(--text-primary));
}

.first-start-actions {
    display: flex;
    align-items: center;
    gap: var(--space-3, 0.75rem);
    margin-top: var(--space-3, 0.75rem);
}

.first-start-apply {
    padding: var(--space-1-5, 0.4rem) var(--space-3, 0.8rem);
    background: var(--accent-primary);
    border: 1px solid var(--accent-primary);
    border-radius: var(--radius-3, 0.25rem);
    color: var(--background-primary);
    font: inherit;
    font-size: var(--font-size-controls, 0.85rem);
    cursor: pointer;
}

.first-start-apply:disabled {
    opacity: 0.6;
    cursor: progress;
}

/* ==== css/dashboard-tag-cloud.css ==== */
/* Dashboard tag word cloud — the / action button's anchored modal (desktop / wide layout only) */

.dashboard-tag-cloud-wrap {
    position: fixed;
    inset: 0;
    z-index: 1001;
    display: none;
    pointer-events: none;
    overflow: visible;
}

body[data-show-tag-cloud-button="true"] .dashboard-tag-cloud-wrap.is-eligible {
    display: block;
}

body[data-show-tag-cloud-button="false"] #tag-cloud-toggle-btn,
#tag-cloud-toggle-btn:not(.is-eligible) {
    display: none !important;
}

body.no-animations .tag-cloud-toggle-btn .search-button-icon {
    animation: none !important;
}

.tag-cloud-toggle-btn.is-active {
    border-color: var(--accent-primary);
    color: var(--text-primary);
}

.tag-cloud-toggle-btn.is-active .search-button-icon {
    color: var(--accent-primary);
}

/* Backdrop (click outside) */
.tag-cloud-modal-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1000;
    background: rgba(0, 0, 0, 0.22);
    opacity: 0;
    transition: opacity 0.18s ease;
    pointer-events: none;
}

.tag-cloud-modal-backdrop.is-open {
    opacity: 1;
    pointer-events: auto;
}

body[data-tag-filter-active="true"] .tag-cloud-modal-backdrop.is-open {
    pointer-events: none;
}

/* Anchored modal — matches dashboard button surfaces */
.tag-cloud-modal {
    position: fixed;
    z-index: 1002;
    height: auto;
    max-height: min(88vh, var(--tag-cloud-modal-max-height, 88vh));
    display: flex;
    flex-direction: column;
    border: 1px solid var(--border-primary);
    background: color-mix(in srgb, var(--background-secondary) 88%, transparent);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border-radius: var(--radius-6, 0.65rem);
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
    opacity: 0;
    transform: translateY(8px) scale(0.97);
    transform-origin: bottom left;
    transition:
        opacity 0.18s ease,
        transform 0.22s ease;
    pointer-events: auto;
    overflow: hidden;
}

.tag-cloud-modal.is-open {
    opacity: 1;
    transform: translateY(0) scale(1);
}

/* The closed cloud is laid out until this rule says otherwise.
   closeModal() sets the `hidden` attribute, and the display above is a class
   rule, which outranks the browser's own `[hidden] { display: none }`. So the
   modal stayed on the grid at opacity 0 with pointer-events on -- an invisible
   541 x 521 box that swallowed every click meant for the bookmarks under it,
   and an aria-hidden container the keyboard could still walk into.
   The backdrop escaped it only because its rule never sets display. */
.tag-cloud-modal[hidden] {
    display: none;
}

.tag-cloud-modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2, 0.5rem);
    padding: var(--space-2, 0.55rem) var(--space-3, 0.65rem) var(--space-1-5, 0.35rem);
    border-bottom: 1px solid var(--border-primary);
    flex-shrink: 0;
}

.tag-cloud-modal-title {
    font-size: var(--font-size-controls);
    font-weight: var(--font-weight-semibold);
    color: var(--text-secondary);
}

.tag-cloud-modal-close {
    border: 1px solid transparent;
    background: transparent;
    color: var(--text-tertiary);
    font-size: var(--text-xl, 1.25rem);
    line-height: 1;
    cursor: pointer;
    padding: var(--space-hairline, 0.15rem) var(--space-1-5, 0.35rem);
    border-radius: var(--radius-4, 0.35rem);
    transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.tag-cloud-modal-close:hover {
    color: var(--accent-primary);
    border-color: var(--border-primary);
    background: color-mix(in srgb, var(--background-primary) 94%, transparent);
}

.tag-cloud-modal-body {
    flex: 0 1 auto;
    overflow-y: hidden;
    padding: var(--space-4, 1rem) var(--space-3, 0.85rem) var(--space-3, 0.75rem);
    min-height: 4.5rem;
    scrollbar-width: thin;
}

.tag-cloud-modal-body.is-scrollable {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
}

/* Word cloud: wrapped rows, accent palette, gentle drift */
.tag-cloud-wordcloud {
    display: flex;
    flex-wrap: wrap;
    align-content: flex-start;
    justify-content: center;
    align-items: baseline;
    gap: var(--space-2, 0.55rem) var(--space-3, 0.75rem);
    text-align: center;
    line-height: 1.35;
}

@keyframes tagCloudWordEnter {
    from {
        opacity: 0;
        transform: rotate(var(--tag-rotate, 0deg)) translateY(6px) scale(0.92);
    }
    to {
        opacity: 1;
        transform: rotate(var(--tag-rotate, 0deg)) translateY(0) scale(1);
    }
}

@keyframes tagCloudWordDrift {
    0%,
    100% {
        transform: rotate(var(--tag-rotate, 0deg)) translateY(0);
    }
    50% {
        transform: rotate(var(--tag-rotate, 0deg)) translateY(-2px);
    }
}

.tag-cloud-word {
    display: inline-flex;
    align-items: baseline;
    gap: 0.1em;
    border: none;
    background: transparent;
    font-family: var(--font-family-main);
    font-size: calc(0.82rem + var(--tag-scale, 0.5) * 1.55rem);
    font-weight: calc(500 + var(--tag-scale, 0.5) * 300);
    padding: 0.2em 0.28em;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    letter-spacing: 0.02em;
    white-space: nowrap;
    transition:
        color 0.15s ease,
        text-shadow 0.15s ease,
        transform 0.2s ease;
}

.tag-cloud-wordcloud--live .tag-cloud-word {
    animation:
        tagCloudWordEnter 0.32s ease backwards,
        tagCloudWordDrift 4.5s ease-in-out infinite;
    animation-delay:
        calc(var(--tag-index, 0) * 0.025s),
        calc(0.35s + var(--tag-index, 0) * 0.11s);
}

body.no-animations .tag-cloud-wordcloud--live .tag-cloud-word {
    animation: none !important;
}

.tag-cloud-word-hash {
    font-weight: var(--font-weight-bold);
    font-size: 0.9em;
    color: color-mix(in srgb, var(--accent-primary) 82%, var(--text-secondary));
}

.tag-cloud-word-label {
    line-height: 1.2;
    color: color-mix(
        in srgb,
        var(--text-secondary) calc((1 - var(--tag-scale, 0.5)) * 45%),
        var(--accent-primary) calc(25% + var(--tag-scale, 0.5) * 55%)
    );
}

.tag-cloud-word--tier-xl .tag-cloud-word-label {
    color: var(--accent-primary);
}

.tag-cloud-word--tier-lg .tag-cloud-word-label {
    color: color-mix(in srgb, var(--accent-primary) 88%, var(--text-primary));
}

.tag-cloud-word--tier-xs .tag-cloud-word-label {
    color: var(--text-secondary);
}

.tag-cloud-word--tier-xl {
    letter-spacing: 0.03em;
}

.tag-cloud-word:hover {
    transform: rotate(var(--tag-rotate, 0deg)) scale(1.06) translateY(-1px);
}

.tag-cloud-word:hover .tag-cloud-word-hash,
.tag-cloud-word:hover .tag-cloud-word-label {
    color: var(--accent-primary);
    text-shadow: 0 0 12px color-mix(in srgb, var(--accent-primary) 45%, transparent);
}

/*
 * A tag in the filter is a filled pill, which is what the overlays spec draws.
 *
 * It used to be the accent colour with an underline under it. In a cloud where
 * size and colour already carry how often a tag is used, an underline is a
 * third quiet signal competing with two loud ones -- and the one thing that is
 * not about frequency at all is which tags are filtering the page right now.
 * A filled pill is the only shape in the cloud, so it cannot be mistaken for a
 * heavier tag.
 */
.tag-cloud-word.is-selected {
    background: var(--accent-primary);
    border-radius: var(--radius-pill, 999px);
    padding: 0.15em 0.6em;
    font-weight: 700;
    box-shadow: 0 0 12px color-mix(in srgb, var(--accent-primary) 40%, transparent);
}

.tag-cloud-word.is-selected .tag-cloud-word-hash,
.tag-cloud-word.is-selected .tag-cloud-word-label {
    /* On the accent, not in it: the page colour is what stays readable on a
       filled accent across every theme. */
    color: var(--background-primary);
}

/* The hover lift is a tag offering itself; a filled one is already taken. */
.tag-cloud-word.is-selected:hover .tag-cloud-word-hash,
.tag-cloud-word.is-selected:hover .tag-cloud-word-label {
    color: var(--background-primary);
    text-shadow: none;
}

/*
 * What the cloud adds up to, on a rule under it.
 *
 * The spec closes the cloud with two figures at opposite ends: how many tags
 * there are, and how many are filtering. Its own flex line, so it sits under
 * the words rather than wrapping among them.
 */
.tag-cloud-total {
    flex-basis: 100%;
    display: flex;
    justify-content: space-between;
    gap: var(--space-3, 0.75rem);
    margin-top: var(--space-3, 0.75rem);
    padding-top: var(--space-2, 0.5rem);
    border-top: 1px solid color-mix(in srgb, var(--border-primary) 45%, transparent);
    font-size: var(--text-2xs, 0.62rem);
    color: var(--text-tertiary);
    opacity: 0.6;
    white-space: nowrap;
}

.tag-cloud-word:focus,
.tag-cloud-word:focus-visible,
.tag-cloud-word.is-keyboard-focused {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
}

.tag-cloud-modal-empty {
    margin: 0;
    font-size: var(--font-size-small);
    color: var(--text-tertiary);
    text-align: center;
}

.tag-cloud-modal-footer {
    padding: var(--space-1-5, 0.35rem) var(--space-3, 0.65rem) var(--space-2, 0.55rem);
    flex-shrink: 0;
}

.tag-cloud-clear-filter {
    width: 100%;
    border: 1px solid var(--border-primary);
    background: color-mix(in srgb, var(--background-secondary) 74%, transparent);
    color: var(--text-secondary);
    font-family: var(--font-family-main);
    font-size: var(--font-size-small);
    padding: var(--space-1-5, 0.35rem) var(--space-2, 0.5rem);
    border-radius: var(--radius-6, 0.65rem);
    cursor: pointer;
    transition:
        background-color 0.2s ease,
        border-color 0.2s ease,
        color 0.2s ease,
        transform 0.2s ease,
        box-shadow 0.2s ease;
}

.tag-cloud-clear-filter:hover {
    background: color-mix(in srgb, var(--background-primary) 94%, transparent);
    border-color: var(--accent-primary);
    color: var(--text-primary);
    transform: translateY(-1px);
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
}

.tag-cloud-clear-filter:focus,
.tag-cloud-clear-filter:focus-visible,
.tag-cloud-clear-filter.is-keyboard-focused {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
    border-color: var(--accent-primary);
    color: var(--text-primary);
}

/* ==== css/feed-row.css ==== */
/* The bookmark feed row, shared by Health, Inbox and Config → Bookmarks.
 *
 * The three used to carry their own copy of this rule — `.health-view-item`,
 * `.inbox-item` and `.config-bm-item` — and the copies had drifted into being
 * byte-identical, which is the worst of both: one design, three places to change
 * it, and no way to tell whether a difference was meant.
 *
 * What genuinely differs between the three is expressed as modifiers below: the
 * checkbox column Health keeps in the grid, and the coloured left edge each view
 * uses for its own state (unread, broken, ticked).
 *
 * Loaded before health-view.css, config-view.css and dashboard-inbox.css so
 * those files can still override where a view really needs it.
 */

.feed-row {
    position: relative;
    display: grid;
    grid-template-columns: 3rem 1fr;
    gap: var(--space-3, 0.85rem);
    padding: var(--space-3, 0.75rem) var(--space-3, 0.85rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--layout-radius-sm, 8px);
    background: var(--background-secondary);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

/* A row with its menu open has to win against the rows below it, and under a
   depth theme it cannot: theme-character.css gives `.feed-row` a
   backdrop-filter, which — like the actions bar's non-1 opacity below — makes
   the row its own stacking context. The menu's z-index: 20 (health-view.css)
   then only ever competes inside that context, so a later row in the DOM
   always paints over it regardless of the number.
   Raising the row itself, only while it holds an open menu, lets it beat the
   later siblings' plain DOM-order stacking. Keyed on the same
   `.health-view-menu:not([hidden])` state the overflow-escape rule below
   already reads, so Health and Config → Bookmarks — the two `.feed-row`
   consumers with an in-row menu — both pick it up for free. Inbox's popovers
   are appended to <body>, never to the row, so they never needed this.

   A plain `z-index: 1` only ever beat a sibling row's default `auto`. It lost
   to list-view-shell.css's sticky `.lvs-header` (--layer-sticky-band, 5), so
   a menu that opens upward across a scrolled-under header still drew behind
   it. --layer-row-raised (layer-tokens.css) is defined to sit above
   --layer-sticky-band for exactly this: the row has to win against the
   sticky band above it, not just the plain rows beside it. */
.feed-row:has(.health-view-menu:not([hidden])) {
    /* One above .lvs-header, whose own z-index was lifted by 1300 to clear
       the teaching popovers (list-view-shell.css) -- at plain
       --layer-row-raised the header drew over any menu crossing it again. */
    z-index: calc(var(--layer-sticky-band, 5) + 1301);
}

/* Health keeps the checkbox in the grid rather than floating it over the icon:
   its rows are scanned in bulk, so the column is always there and only fades. */
.feed-row--with-select {
    grid-template-columns: 1.1rem 3rem 1fr;
}

.feed-row:hover {
    border-color: color-mix(in srgb, var(--accent-primary) 42%, var(--border-primary));
    background: color-mix(in srgb, var(--accent-primary) 7%, var(--background-secondary));
}

.feed-row.keyboard-selected {
    border-color: var(--accent-primary);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-primary) 28%, transparent);
    background: color-mix(in srgb, var(--accent-primary) 9%, var(--background-secondary));
}

/* The flash after jumping to a row from somewhere else. */
.feed-row--highlight {
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-primary) 35%, transparent);
}

/* Classic layout has no focus ring on the row at all — the keyboard cursor is
   drawn by .keyboard-selected, but Tab-ing to a row with tabindex="-1" showed
   nothing. Modern layout already had this; now both do. */
.feed-row:focus-visible {
    outline: var(--layout-focus-ring, 2px solid color-mix(in srgb, var(--accent-primary) 72%, transparent));
    outline-offset: 2px;
}

/* The state edge. Each view names its own state; the shape is shared. */
.feed-row--edge-accent {
    border-left: 3px solid var(--accent-primary);
}

.feed-row--edge-error {
    border-left: 3px solid var(--accent-error);
}

.feed-row--edge-warning {
    border-left: 3px solid var(--accent-warning);
}

/* The feed itself: a column of cards, no dividers and no striping — the card
   border and the gap do the separating. */
.feed-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-3, 0.65rem);
}

/* The row's action bar, revealed on hover, focus or the keyboard cursor.
   grid-template-rows rather than height, so it animates from nothing to its
   own content height without a magic number. */
.feed-row-actions {
    /* Read by the children below. Reset here so a bar never inherits an open
       state from an outer one. */
    --feed-row-actions-child-overflow: hidden;
    display: grid;
    grid-template-rows: 0fr;
    margin-top: 0;
    opacity: 0;
    transition:
        grid-template-rows 0.14s ease,
        opacity 0.12s ease,
        margin-top 0.14s ease;
}

.feed-row-actions > * {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1-5, 0.35rem);
    min-height: 0;
    overflow: var(--feed-row-actions-child-overflow);
}

.feed-row:hover .feed-row-actions,
.feed-row:focus-within .feed-row-actions,
.feed-row.keyboard-selected .feed-row-actions {
    grid-template-rows: 1fr;
    margin-top: var(--space-2, 0.55rem);
    opacity: 1;
}

/* The bar collapses to nothing — grid-template-rows: 0fr, which the
   min-height: 0/overflow: hidden above ride down to zero — the moment the
   pointer leaves the row, which an open menu's own dropdown does immediately,
   on the way to one of its items: the menu would vanish under the reader's
   cursor. Holding the bar at its revealed state, matching the :hover rule
   above, keeps it (and the menu inside it) on screen for as long as the menu
   is open, whether or not the row is still hovered.
   The collapsing wrapper also clips its content, which would cut off an open
   menu even while the bar itself stays revealed; only while a menu is open
   does the row let it escape — the height animation is finished by then, so
   nothing jumps. Matches both the shared menu class and the one Health names
   its own popovers with.
   The children are let out through a variable rather than a
   `.feed-row-actions:has(...) > *` rule. A :has() that is not the subject of
   its selector made Chrome restyle the whole page on every DOM change
   anywhere: 16x the style work while the grid renders. */
.feed-row-actions:has(.health-view-menu:not([hidden])) {
    --feed-row-actions-child-overflow: visible;
    grid-template-rows: 1fr;
    margin-top: var(--space-2, 0.55rem);
    opacity: 1;
    overflow: visible;
}

/* Nothing hovers on a touch screen, and the actions are the point of the row. */
@media (hover: none) {
    .feed-row-actions {
        grid-template-rows: 1fr;
        margin-top: var(--space-2, 0.55rem);
        opacity: 1;
    }
}

@media (max-width: 520px) {
    .feed-row-actions {
        grid-template-rows: 1fr;
        margin-top: var(--space-2, 0.55rem);
        opacity: 1;
    }
}

/*
 * Row anatomy: alignment and density only.
 *
 * Columns deliberately stay with the base rule and with --with-select. Both
 * declare grid-template-columns at the same specificity, so a third
 * declaration here would win by source order and silently drop the checkbox
 * column from every health row.
 */
.feed-row--grid {
    align-items: start;
}

/* For a view that fills a trailing column, such as a score or a status pill. */
.feed-row--grid-3 {
    grid-template-columns: 3rem 1fr auto;
}

/* Forward scaffolding: no view emits --grid-3 yet (only
 * tests/feed-row-grid.spec.js synthesises it), but a future row that carries
 * --with-select and wants a trailing column (a score, a status pill) would
 * hit exactly this pair. Both rules sit at equal specificity and --grid-3
 * comes later in the file, so without a rule for the combination, --grid-3
 * would win by source order and silently drop the checkbox track — the same
 * collision --grid was split out above to avoid for the --grid/--with-select
 * pair.
 */
.feed-row--with-select.feed-row--grid-3 {
    grid-template-columns: 1.1rem 3rem 1fr auto;
}

/*
 * Row height follows the one density setting, not a second one.
 *
 * These read data-list-density, which this app no longer has: the rows and the
 * dashboard's cards are the same reading preference and now share
 * densityMode. Four values rather than two, because that is what the setting
 * holds -- and `auto` sits with compact, which is what it resolved to before
 * anyone could choose it here.
 */
body[data-density-mode="compact"] .feed-row--grid,
body[data-density-mode="auto"] .feed-row--grid {
    padding-block: var(--space-1);
}

body[data-density-mode="comfortable"] .feed-row--grid {
    padding-block: var(--space-3);
}

body[data-density-mode="dense"] .feed-row--grid {
    padding-block: var(--space-hairline, 0.12rem);
}

/* ==== css/spread-tutorial.css ==== */
/**
 * The spread-across-columns walkthrough.
 *
 * Same shape as inbox-tutorial.css and health-tutorial.css: the app's modal
 * chrome carries the title and the buttons, and only the reading column between
 * them is styled here. Its own sheet, so no tour's visuals drift into another's.
 *
 * The visuals in this one are diagrams rather than copies of a control — the
 * subject is a shape on the page, so stand-in columns and rows say more than a
 * screenshot of a menu would.
 */
body:has(#dashboard-layout) #app-modal.modal-overlay:has(.spread-tutorial-modal) .modal {
    transform: translateY(10px) scale(0.98);
    transition: transform 0.15s ease-in, opacity 0.15s ease-in;
}

body:has(#dashboard-layout) #app-modal.modal-overlay.show:has(.spread-tutorial-modal) .modal {
    transform: translateY(0) scale(1);
    transition: transform 0.38s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.22s ease-out;
}

body.no-animations:has(#dashboard-layout) #app-modal.modal-overlay:has(.spread-tutorial-modal) .modal {
    transform: none !important;
    transition: opacity 0.15s ease !important;
}

.spread-tutorial-progress {
    font-size: var(--text-xs, 0.72rem);
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    margin: 0 0 var(--space-1-5, 0.4rem);
}

.spread-tutorial-step-title {
    margin: 0 0 var(--space-3, 0.7rem);
    font-size: var(--text-lg, 1.1rem);
    color: var(--text-primary);
}

.spread-tutorial-step-body p {
    margin: 0 0 var(--space-3, 0.7rem);
    color: var(--text-secondary);
    line-height: 1.55;
}

.spread-tutorial-step-body p:last-child {
    margin-bottom: 0;
}

.spread-tutorial-visual {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    padding: var(--space-3, 0.75rem);
    margin: 0 0 var(--space-4, 0.9rem);
    border: 1px dashed var(--border-secondary);
    border-radius: var(--radius-5, 0.5rem);
    background: var(--background-secondary);
}

.spread-tutorial-visual--compare,
.spread-tutorial-visual--sum {
    justify-content: center;
}

.spread-tutorial-block {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-1, 0.3rem);
}

.spread-tutorial-cap {
    font-size: var(--text-2xs, 0.66rem);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}

/* The stand-in category: columns of blank rows, at the proportions a real one
   has, so the before/after difference is a shape rather than a sentence. */
.spread-tutorial-grid {
    display: flex;
    gap: var(--space-1, 0.3rem);
    padding: var(--space-1-5, 0.35rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-4, 0.35rem);
    background: var(--background-primary);
}

.spread-tutorial-col {
    display: flex;
    flex-direction: column;
    gap: var(--space-1, 0.25rem);
    width: 3.1rem;
}

.spread-tutorial-row {
    height: 0.4rem;
    border-radius: var(--radius-1, 0.15rem);
    background: color-mix(in srgb, var(--accent-primary) 30%, var(--border-primary));
}

/* Every second row a little shorter: a flat stack of identical bars reads as a
   loading skeleton rather than as bookmarks. */
.spread-tutorial-row:nth-child(even) {
    width: 78%;
}

.spread-tutorial-arrow,
.spread-tutorial-op {
    color: var(--text-tertiary);
    font-size: var(--text-base, 0.95rem);
}

.spread-tutorial-chip {
    padding: var(--space-hairline, 0.15rem) var(--space-2, 0.45rem);
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-pill, 999px);
    font-size: var(--text-xs, 0.72rem);
    color: var(--text-secondary);
    white-space: nowrap;
}

.spread-tutorial-key {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1-5, 0.35rem);
    font-size: var(--text-sm, 0.78rem);
    color: var(--text-secondary);
}

.spread-tutorial-key kbd {
    padding: 0 var(--space-1, 0.3rem);
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-3, 0.25rem);
    font-family: inherit;
    font-size: var(--text-xs, 0.72rem);
    color: var(--text-tertiary);
}

.spread-tutorial-hint {
    flex: 1 1 10rem;
    min-width: 0;
    font-size: var(--text-xs, 0.75rem);
    color: var(--text-tertiary);
}

/* The menu step: three rows of the real right-click menu, with the one this
   tour is about picked out. */
.spread-tutorial-menu {
    display: flex;
    flex-direction: column;
    gap: var(--space-hairline, 0.15rem);
    width: 100%;
    padding: var(--space-1, 0.3rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-5, 0.45rem);
    background: var(--background-primary);
}

.spread-tutorial-menu-row {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.45rem);
    padding: var(--space-1, 0.3rem) var(--space-2, 0.45rem);
    border-radius: var(--radius-3, 0.3rem);
    font-size: var(--text-sm, 0.78rem);
    color: var(--text-tertiary);
}

.spread-tutorial-menu-row.is-current {
    background: color-mix(in srgb, var(--accent-primary) 12%, transparent);
    color: var(--text-primary);
}

.spread-tutorial-menu-row kbd {
    margin-left: auto;
    padding: 0 var(--space-1, 0.25rem);
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-2, 0.2rem);
    font-family: inherit;
    font-size: var(--text-2xs, 0.68rem);
    color: var(--text-tertiary);
}

.spread-tutorial-dots {
    display: flex;
    gap: var(--space-1, 0.3rem);
    margin-top: var(--space-4, 0.9rem);
}

.spread-tutorial-dot {
    width: 0.4rem;
    height: 0.4rem;
    border-radius: 50%;
    background: var(--border-secondary);
}

.spread-tutorial-dot.is-active {
    background: var(--accent-primary);
}

/* ==== css/fresh-tutorial.css ==== */
/**
 * The Fresh walkthrough.
 *
 * Same shape as spread-tutorial.css, inbox-tutorial.css and health-tutorial.css:
 * the app's modal chrome carries the title and the buttons, and only the reading
 * column between them is styled here. Its own sheet, so no tour's visuals drift
 * into another's.
 *
 * The visuals are stand-in bookmark rows rather than diagrams, because what
 * Fresh changes is a row you already know the look of — one small count on the
 * right of it, and a collection made of the same rows.
 */
body:has(#dashboard-layout) #app-modal.modal-overlay:has(.fresh-tutorial-modal) .modal {
    transform: translateY(10px) scale(0.98);
    transition: transform 0.15s ease-in, opacity 0.15s ease-in;
}

body:has(#dashboard-layout) #app-modal.modal-overlay.show:has(.fresh-tutorial-modal) .modal {
    transform: translateY(0) scale(1);
    transition: transform 0.38s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.22s ease-out;
}

body.no-animations:has(#dashboard-layout) #app-modal.modal-overlay:has(.fresh-tutorial-modal) .modal {
    transform: none !important;
    transition: opacity 0.15s ease !important;
}

.fresh-tutorial-progress {
    font-size: var(--text-xs, 0.72rem);
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    margin: 0 0 var(--space-1-5, 0.4rem);
}

.fresh-tutorial-step-title {
    margin: 0 0 var(--space-3, 0.7rem);
    font-size: var(--text-lg, 1.1rem);
    color: var(--text-primary);
}

.fresh-tutorial-step-body p {
    margin: 0 0 var(--space-3, 0.7rem);
    color: var(--text-secondary);
    line-height: 1.55;
}

.fresh-tutorial-step-body p:last-child {
    margin-bottom: 0;
}

.fresh-tutorial-visual {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    padding: var(--space-3, 0.75rem);
    margin: 0 0 var(--space-4, 0.9rem);
    border: 1px dashed var(--border-secondary);
    border-radius: var(--radius-5, 0.5rem);
    background: var(--background-secondary);
}

.fresh-tutorial-visual--flow {
    justify-content: center;
}

.fresh-tutorial-stack,
.fresh-tutorial-collection {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    gap: var(--space-1, 0.3rem);
    padding: var(--space-1-5, 0.4rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-4, 0.35rem);
    background: var(--background-primary);
}

.fresh-tutorial-collection-title {
    display: flex;
    align-items: center;
    gap: var(--space-1, 0.3rem);
    margin-bottom: var(--space-hairline, 0.15rem);
    font-size: var(--text-sm, 0.78rem);
    color: var(--text-secondary);
}

.fresh-tutorial-count {
    color: var(--text-tertiary);
    font-variant-numeric: tabular-nums;
}

/* A stand-in row: icon, name, and the one thing Fresh adds on the right. */
.fresh-tutorial-row {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.45rem);
    padding: var(--space-1, 0.25rem) var(--space-1-5, 0.35rem);
    border-radius: var(--radius-3, 0.25rem);
    background: color-mix(in srgb, var(--accent-primary) 6%, transparent);
}

.fresh-tutorial-icon {
    width: 0.7rem;
    height: 0.7rem;
    flex: 0 0 auto;
    border-radius: var(--radius-2, 0.18rem);
    background: color-mix(in srgb, var(--accent-primary) 45%, transparent);
}

.fresh-tutorial-name {
    flex: 1 1 auto;
    font-size: var(--text-sm, 0.8rem);
    color: var(--text-secondary);
}

/* The count itself, drawn as the row draws it: small, quiet, on the right. */
.fresh-tutorial-badge {
    flex: 0 0 auto;
    min-width: 1.15rem;
    padding: var(--space-hairline, 0.05rem) var(--space-1, 0.3rem);
    border-radius: var(--radius-pill, 999px);
    background: var(--accent-primary);
    color: var(--background-primary);
    font-size: var(--text-2xs, 0.68rem);
    font-weight: var(--font-weight-semibold);
    line-height: 1.4;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

.fresh-tutorial-arrow {
    color: var(--text-tertiary);
    font-size: var(--text-base, 0.95rem);
}

.fresh-tutorial-chip {
    padding: var(--space-hairline, 0.15rem) var(--space-2, 0.45rem);
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-pill, 999px);
    font-size: var(--text-xs, 0.72rem);
    color: var(--text-secondary);
    white-space: nowrap;
}

/* The conditional request's answer: the cheap end of the route, so it is drawn
   as the quiet one. */
.fresh-tutorial-chip--quiet {
    border-style: dashed;
    color: var(--text-tertiary);
    font-family: var(--font-mono, ui-monospace, monospace);
}

.fresh-tutorial-note {
    flex: 1 1 auto;
    font-size: var(--text-sm, 0.8rem);
    line-height: 1.5;
    color: var(--text-secondary);
}

.fresh-tutorial-dots {
    display: flex;
    justify-content: center;
    gap: var(--space-1-5, 0.35rem);
    margin-top: var(--space-4, 0.9rem);
}

.fresh-tutorial-dot {
    width: 0.4rem;
    height: 0.4rem;
    border-radius: var(--radius-pill, 999px);
    background: var(--border-primary);
}

.fresh-tutorial-dot.is-active {
    background: var(--accent-primary);
}

/* ==== css/changes-tour.css ==== */
/*
 * The "what has changed" tour: a step in the app modal, and the spotlight it
 * casts on the real thing behind it.
 *
 * Shaped like the other walkthroughs (fresh, inbox, health): a progress line, a
 * title, one small drawing made from the shapes of the real element, a short
 * paragraph and a row of dots. What it adds is the choice row -- where a
 * default moved, the step carries the setting -- and the two buttons that leave
 * the window: Show me, which dims it, and Open, which goes somewhere.
 */

.changes-tour {
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 0.55rem);
    text-align: left;
}

.changes-tour-progress {
    font-size: var(--font-size-small, 0.72rem);
    color: var(--text-tertiary);
    letter-spacing: 0.04em;
}

.changes-tour-step-title {
    margin: 0;
    font-size: var(--font-size-heading, 1rem);
    color: var(--text-primary);
}

.changes-tour-step-body {
    margin: 0;
    font-size: var(--font-size-text);
    color: var(--text-secondary);
    line-height: 1.55;
}

/* A key named in a step, drawn as a key. */
.changes-tour-step-body kbd {
    padding: 0.05rem 0.3rem;
    border: 1px solid var(--border-secondary, var(--border-primary));
    border-radius: var(--radius-3, 4px);
    background: var(--background-secondary);
    color: var(--text-primary);
    font-family: var(--font-family-main);
    font-size: 0.88em;
    white-space: nowrap;
}

/* ---- the drawings ---------------------------------------------------- */

.changes-tour-art {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 5.5rem;
    padding: var(--space-3, 0.8rem);
    border: 1px solid var(--hairline, var(--border-primary));
    border-radius: var(--radius-5, 9px);
    background: color-mix(in srgb, var(--text-primary) 4%, transparent);
    color: var(--text-secondary);
    font-size: var(--font-size-small, 0.72rem);
}

.changes-tour-dim { color: var(--text-tertiary); }

/* The header, with the tabs where they stand. */
.changes-tour-bar {
    display: flex;
    align-items: center;
    gap: var(--space-3, 0.8rem);
    width: 100%;
    padding: var(--space-2, 0.45rem) var(--space-3, 0.7rem);
    border: 1px solid var(--hairline, var(--border-primary));
    border-radius: var(--radius-4, 7px);
}

.changes-tour-tabs {
    display: flex;
    gap: var(--space-1-5, 0.3rem);
    margin-left: auto;
}

.changes-tour-tabs b,
.changes-tour-tabs i {
    padding: 0.1rem 0.4rem;
    border-radius: var(--radius-3, 5px);
    font-style: normal;
    font-weight: normal;
    color: var(--text-tertiary);
}

.changes-tour-tabs b {
    background: color-mix(in srgb, var(--accent-primary) 18%, transparent);
    color: var(--text-primary);
}

/* The page, with the bar at its right edge and the handle beside it. */
.changes-tour-page {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--space-3, 0.8rem);
    width: 100%;
    min-height: 4.5rem;
    padding: var(--space-2, 0.5rem);
    border: 1px solid var(--hairline, var(--border-primary));
    border-radius: var(--radius-4, 7px);
}

.changes-tour-lines {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    flex: 1;
}

.changes-tour-lines i {
    height: 0.35rem;
    border-radius: 2px;
    background: color-mix(in srgb, var(--text-primary) 12%, transparent);
}

.changes-tour-lines i:nth-child(2) { width: 70%; }
.changes-tour-lines i:nth-child(3) { width: 45%; }

.changes-tour-rail {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    padding: 0.3rem;
    border: 1px solid var(--hairline, var(--border-primary));
    border-radius: var(--radius-4, 7px);
    background: color-mix(in srgb, var(--text-primary) 5%, transparent);
}

.changes-tour-rail i {
    display: grid;
    place-items: center;
    width: 1.2rem;
    height: 1.2rem;
    border-radius: 4px;
    font-style: normal;
    color: var(--text-tertiary);
}

.changes-tour-handle {
    width: 0.5rem;
    height: 2.4rem;
    border-radius: 4px 0 0 4px;
    background: color-mix(in srgb, var(--text-primary) 22%, transparent);
}

/* Config's tiles, a widget's figures. */
.changes-tour-tiles {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.4rem;
    width: 100%;
}

.changes-tour-tiles i {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    padding: 0.3rem 0.4rem;
    min-width: 0;
    border: 1px solid var(--hairline, var(--border-primary));
    border-radius: var(--radius-4, 6px);
    font-style: normal;
}

.changes-tour-tiles b {
    font-weight: var(--font-weight-semibold, 600);
    font-size: 0.92em;
    color: var(--text-primary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.changes-tour-tiles em {
    font-style: normal;
    font-size: 0.85em;
    color: var(--text-tertiary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}



/* The column's own name, then what stands in it: a tick, or a row. */




.changes-tour-widget {
    display: grid;
    grid-template-columns: auto auto;
    gap: 0.1rem var(--space-3, 0.9rem);
    align-items: baseline;
}

.changes-tour-widget b {
    font-size: 1.5em;
    font-weight: normal;
    color: var(--text-primary);
}

.changes-tour-tags {
    display: flex;
    gap: 0.35rem;
    flex-wrap: wrap;
}

.changes-tour-tags i {
    padding: 0.15rem 0.5rem;
    border-radius: 999px;
    font-style: normal;
    background: color-mix(in srgb, var(--text-primary) 10%, transparent);
}

.changes-tour-tags i.is-own {
    background: none;
    border: 1px dashed var(--hairline, var(--border-primary));
    color: var(--text-tertiary);
}

.changes-tour-swatches {
    display: flex;
    gap: 0.6rem;
}

/*
 * A theme card: a ground, a row of its accents, and for the lacquered one a
 * lit band across the top and the badge the theme browser puts on it.
 */
.changes-tour-swatches i {
    position: relative;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    gap: 0.3rem;
    width: 4.6rem;
    height: 3.2rem;
    padding: 0.35rem;
    border-radius: var(--radius-5, 9px);
    border: 1px solid var(--hairline, var(--border-primary));
    overflow: hidden;
    font-style: normal;
}

.changes-tour-chips {
    display: flex;
    gap: 0.2rem;
}

.changes-tour-chips u {
    flex: 1;
    height: 0.45rem;
    border-radius: 2px;
    text-decoration: none;
}

.changes-tour-chips u:nth-child(1) { background: var(--accent-primary); }
.changes-tour-chips u:nth-child(2) { background: var(--accent-success, var(--accent-primary)); }
.changes-tour-chips u:nth-child(3) { background: var(--accent-warning, var(--accent-primary)); }

.changes-tour-swatches i.is-lacquer {
    background:
        linear-gradient(180deg,
            color-mix(in srgb, white 34%, transparent) 0%,
            color-mix(in srgb, white 6%, transparent) 38%,
            transparent 58%),
        linear-gradient(160deg,
            color-mix(in srgb, var(--accent-primary) 55%, transparent) 0%,
            color-mix(in srgb, var(--accent-primary) 18%, var(--background-secondary)) 100%);
}

/* The character badge the theme browser puts on a lacquered family. */
.changes-tour-swatches i.is-lacquer b {
    position: absolute;
    top: 0.3rem;
    left: 0.35rem;
    padding: 0 0.28rem;
    border-radius: 999px;
    background: color-mix(in srgb, var(--background-primary) 70%, transparent);
    color: var(--text-primary);
    font-size: 0.62rem;
    font-weight: var(--font-weight-semibold, 600);
    letter-spacing: 0.02em;
}

.changes-tour-swatches i.is-light {
    background: color-mix(in srgb, white 88%, var(--accent-primary));
}

.changes-tour-swatches i.is-dark {
    background: color-mix(in srgb, black 82%, var(--accent-primary));
}

.changes-tour-done {
    font-size: 1.8rem;
    color: var(--accent-primary);
}

/* ---- the choice, and the two buttons that leave ---------------------- */

.changes-tour-choice-row {
    display: flex;
    flex-direction: column;
    gap: var(--space-1-5, 0.35rem);
}

.changes-tour-choice-label {
    font-size: var(--font-size-small, 0.72rem);
    color: var(--text-tertiary);
}

.changes-tour-choices,
.changes-tour-extra {
    display: flex;
    gap: var(--space-1-5, 0.35rem);
    flex-wrap: wrap;
}

.changes-tour-choice,
.changes-tour-extra-btn {
    padding: var(--space-1, 0.3rem) var(--space-2, 0.6rem);
    font-family: inherit;
    font-size: var(--font-size-small, 0.74rem);
    color: var(--text-secondary);
    background: var(--background-primary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-4, 0.35rem);
    cursor: pointer;
}

.changes-tour-choice[aria-pressed="true"] {
    color: var(--text-primary);
    border-color: var(--accent-primary);
    background: color-mix(in srgb, var(--accent-primary) 14%, transparent);
}

.changes-tour-dots {
    display: flex;
    gap: 0.3rem;
    margin-top: var(--space-1, 0.25rem);
}

.changes-tour-dot {
    width: 0.4rem;
    height: 0.4rem;
    border-radius: 50%;
    background: var(--border-primary);
}

.changes-tour-dot.is-active { background: var(--accent-primary); }

/* ---- Show me: the window steps aside and the real thing lights up ---- */

body.changes-tour-peeking .modal-overlay {
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.15s ease;
}

/*
 * The sheet over the page while something on it is lit.
 *
 * Under the lit element and over everything else, so the thing being pointed
 * at is visible and nothing on the dashboard can be pressed by accident. The
 * dimming itself is the lit element's own outer shadow.
 */
.changes-tour-peek-guard {
    position: fixed;
    inset: 0;
    z-index: 2150;
    background: transparent;
    cursor: default;
}

.changes-tour-lit {
    position: relative;
    z-index: 2200;
    border-radius: var(--radius-5, 9px);
    box-shadow: 0 0 0 3px var(--accent-primary), 0 0 0 9999px rgba(0, 0, 0, 0.45);
}

@media (prefers-reduced-motion: no-preference) {
    /*
 * The sheet over the page while something on it is lit.
 *
 * Under the lit element and over everything else, so the thing being pointed
 * at is visible and nothing on the dashboard can be pressed by accident. The
 * dimming itself is the lit element's own outer shadow.
 */
.changes-tour-peek-guard {
    position: fixed;
    inset: 0;
    z-index: 2150;
    background: transparent;
    cursor: default;
}

.changes-tour-lit { animation: changes-tour-pulse 1.1s ease-in-out 2; }
}

@keyframes changes-tour-pulse {
    50% { box-shadow: 0 0 0 6px var(--accent-primary), 0 0 0 9999px rgba(0, 0, 0, 0.45); }
}

/* ==== css/digest.css ==== */
/*
 * The weekly digest's window (static/js/digest-notice.js).
 *
 * A part per subject: a title with the button to the view that deals with it,
 * one line of what it means, and the names behind the count as chips. The
 * window scrolls inside the modal like any other; nothing here is fixed.
 */
.digest-window {
    display: flex;
    flex-direction: column;
    text-align: left;
}

.digest-window-when {
    margin: 0;
    font-size: var(--font-size-small, 0.72rem);
    color: var(--text-tertiary);
}

.digest-window-intro {
    margin: var(--space-1, 0.3rem) 0 var(--space-2, 0.55rem);
    font-size: var(--font-size-text);
    color: var(--text-secondary);
    line-height: 1.5;
}

.digest-part {
    padding: var(--space-3, 0.8rem) 0;
    border-top: 1px solid var(--hairline, var(--border-primary));
}

.digest-part-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2, 0.55rem);
}

.digest-part-title {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.55rem);
    margin: 0;
    font-size: var(--font-size-text);
    font-weight: 600;
    color: var(--text-primary);
}

.digest-part-icon {
    width: 1.2em;
    text-align: center;
    color: var(--text-secondary);
}

.digest-part-go {
    flex: none;
    padding: 0.3rem 0.65rem;
    border: 1px solid var(--border-secondary, var(--border-primary));
    border-radius: var(--radius-4, 6px);
    background: transparent;
    color: var(--text-primary);
    font: inherit;
    font-size: var(--font-size-small, 0.8rem);
    cursor: pointer;
    white-space: nowrap;
}

.digest-part-go:hover {
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
}

.digest-part-go:focus-visible {
    outline: 2px solid var(--accent-primary, var(--accent-color, #4a90d9));
    outline-offset: 1px;
}

.digest-part-note {
    margin: var(--space-1, 0.3rem) 0 0;
    font-size: var(--font-size-small, 0.8rem);
    color: var(--text-secondary);
    line-height: 1.5;
}

.digest-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    margin-top: var(--space-2, 0.55rem);
}

.digest-chip {
    padding: 0.12rem 0.5rem;
    border-radius: var(--radius-3, 4px);
    background: color-mix(in srgb, var(--text-primary) 7%, transparent);
    color: var(--text-primary);
    font-size: var(--font-size-small, 0.78rem);
    overflow-wrap: anywhere;
}

.digest-chip--more {
    background: transparent;
    color: var(--text-tertiary);
}

/* A part already opened from this digest, and the one to do next. */
.digest-part.is-done .digest-part-title,
.digest-part.is-done .digest-part-note {
    color: var(--text-tertiary);
}

.digest-part.is-done .digest-part-icon {
    color: var(--accent-primary, var(--accent-color, currentColor));
}

.digest-part-go.is-next {
    border-color: var(--accent-primary, var(--accent-color, currentColor));
    color: var(--accent-primary, var(--accent-color, currentColor));
}

/*
 * The minimised digest, in the corner the notice cards use (dashboard-
 * quickstart.css). Under the modal, unlike the cards, because the window it
 * reopens is that modal.
 */
.digest-bar {
    position: fixed;
    left: max(1rem, env(safe-area-inset-left));
    bottom: max(1rem, env(safe-area-inset-bottom));
    z-index: 1500;
    display: flex;
    align-items: center;
    gap: 0.4rem;
    max-width: calc(100vw - 2rem);
    padding: 0.3rem 0.35rem 0.3rem 0.4rem;
    border: 1px solid var(--border-secondary, var(--border-primary));
    border-radius: 999px;
    background: var(--background-primary);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.22);
    font-size: var(--font-size-small, 0.8rem);
}

.digest-bar button {
    font: inherit;
    cursor: pointer;
}

.digest-bar-open {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    min-width: 0;
    padding: 0.2rem 0.5rem;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--text-primary);
}

.digest-bar-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.digest-bar-meter {
    flex: none;
    width: 3.5rem;
    height: 4px;
    border-radius: 2px;
    background: color-mix(in srgb, var(--text-primary) 14%, transparent);
    overflow: hidden;
}

.digest-bar-meter > span {
    display: block;
    height: 100%;
    background: var(--accent-primary, var(--accent-color, currentColor));
}

.digest-bar-next {
    flex: none;
    padding: 0.25rem 0.7rem;
    border: 1px solid var(--accent-primary, var(--accent-color, currentColor));
    border-radius: 999px;
    background: transparent;
    color: var(--accent-primary, var(--accent-color, currentColor));
    white-space: nowrap;
}

.digest-bar-close {
    flex: none;
    width: 1.7rem;
    height: 1.7rem;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--text-tertiary);
    font-size: 1rem;
    line-height: 1;
}

.digest-bar button:hover {
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
}

.digest-bar button:focus-visible {
    outline: 2px solid var(--accent-primary, var(--accent-color, #4a90d9));
    outline-offset: 1px;
}

/* On a phone the label gives way first; the next step and the × stay. */
@media (max-width: 560px) {
    .digest-bar {
        left: 0.75rem;
        right: 0.75rem;
        max-width: none;
    }

    .digest-bar-meter {
        display: none;
    }
}

.digest-part-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 0.35rem;
}

/* ==== css/dashboard-tutorial.css ==== */
/**
 * The one-time dashboard tutorial modal.
 *
 * Same shape as inbox-tutorial.css: the app's modal chrome carries the title
 * and buttons, and only the reading column between them is styled here. Kept
 * as its own sheet so neither tour's visuals can drift into the other's.
 */
body:has(#dashboard-layout) #app-modal.modal-overlay:has(.dashboard-tutorial-modal) .modal {
    transform: translateY(10px) scale(0.98);
    transition: transform 0.15s ease-in, opacity 0.15s ease-in;
}

body:has(#dashboard-layout) #app-modal.modal-overlay.show:has(.dashboard-tutorial-modal) .modal {
    transform: translateY(0) scale(1);
    transition: transform 0.38s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.22s ease-out;
}

body.no-animations:has(#dashboard-layout) #app-modal.modal-overlay:has(.dashboard-tutorial-modal) .modal {
    transform: none !important;
    transition: opacity 0.15s ease !important;
}

.dashboard-tutorial-progress {
    font-size: var(--text-xs, 0.72rem);
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: var(--text-tertiary);
    margin: 0 0 var(--space-1-5, 0.4rem);
}

.dashboard-tutorial-step-title {
    margin: 0 0 var(--space-3, 0.7rem);
    font-size: var(--text-lg, 1.1rem);
    color: var(--text-primary);
}

.dashboard-tutorial-step-body p {
    margin: 0 0 var(--space-3, 0.7rem);
    color: var(--text-secondary);
    line-height: 1.55;
}

.dashboard-tutorial-step-body p:last-child {
    margin-bottom: 0;
}

.dashboard-tutorial-closing {
    color: var(--text-primary) !important;
    font-style: italic;
}

.dashboard-tutorial-dots {
    display: flex;
    justify-content: center;
    gap: var(--space-1-5, 0.4rem);
    margin-top: 1.2rem;
}

.dashboard-tutorial-dot {
    width: 0.4rem;
    height: 0.4rem;
    border-radius: var(--radius-pill, 999px);
    background: var(--border-secondary);
    transition: background-color 0.15s ease, transform 0.15s ease;
}

.dashboard-tutorial-dot.is-active {
    background: var(--accent-primary);
    transform: scale(1.3);
}

@media (prefers-reduced-motion: reduce) {
    body:has(#dashboard-layout) #app-modal.modal-overlay:has(.dashboard-tutorial-modal) .modal,
    body:has(#dashboard-layout) #app-modal.modal-overlay.show:has(.dashboard-tutorial-modal) .modal {
        transform: none !important;
        transition: opacity 0.15s ease !important;
    }

    .dashboard-tutorial-dot {
        transition: none;
    }
}

/* The scenes carry their own styles, inline in each SVG: see dashboard-tutorial.js. */
.dashboard-tutorial-scene {
    margin: 0 0 var(--space-4, 0.9rem);
    padding: var(--space-2, 0.5rem);
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-5, 0.5rem);
    background: var(--background-primary);
}

.dashboard-tutorial-scene .dtv {
    display: block;
    width: 100%;
    height: auto;
}

/* Each step's scene slides in from the side the reader is heading: Next from
   the right, Back from the left. The motion inside the scene is the SVG's own
   (see dashboard-tutorial.js); this is only the change of page. */
.dashboard-tutorial-scene.is-forward {
    animation: dashboard-tutorial-scene-in 0.32s cubic-bezier(0.16, 1, 0.3, 1);
}

.dashboard-tutorial-scene.is-back {
    animation: dashboard-tutorial-scene-in-back 0.32s cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes dashboard-tutorial-scene-in {
    from { opacity: 0; transform: translateX(14px); }
    to { opacity: 1; transform: none; }
}

@keyframes dashboard-tutorial-scene-in-back {
    from { opacity: 0; transform: translateX(-14px); }
    to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .dashboard-tutorial-scene.is-forward,
    .dashboard-tutorial-scene.is-back {
        animation: none;
    }
}

body.no-animations .dashboard-tutorial-scene.is-forward,
body.no-animations .dashboard-tutorial-scene.is-back {
    animation: none;
}

/* ==== css/setting-art.css ==== */
/* ─── Setting art ───────────────────────────────────────────────────────────
   Stand-in drawings for the settings whose difference is a shape: how many
   columns, how much room between rows, how wide the margins are. Same grammar
   as the spread tour's visuals — blank bars at the proportions of the real
   thing — so config, help and the tour all draw a bookmark the same way.

   Decorative: every drawing sits beside the label that names it, so the markup
   is aria-hidden and none of this is in the accessibility tree. */

.setting-art {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1-5, 0.4rem);
    pointer-events: none;
}

.setting-art-frame {
    display: flex;
    align-items: center;
    gap: var(--space-1, 0.25rem);
    padding: var(--space-1, 0.3rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-4, 0.35rem);
    background: var(--background-primary);
}

.setting-art-col {
    display: flex;
    flex-direction: column;
    gap: var(--space-1, 0.2rem);
    width: 1.5rem;
}

.setting-art-row {
    height: 0.28rem;
    border-radius: var(--radius-1, 0.12rem);
    background: color-mix(in srgb, var(--accent-primary) 30%, var(--border-primary));
}

/* A stack of identical bars reads as a loading skeleton; the short one every
   other line reads as text. */
.setting-art-row:nth-child(even) {
    width: 76%;
}

.setting-art-more {
    align-self: flex-end;
    font-size: var(--text-xs, 0.7rem);
    line-height: 1;
    color: var(--text-tertiary);
}

/* Density is row height and row gap, and nothing else. */
.setting-art-col.is-comfortable { gap: var(--space-1, 0.24rem); }
.setting-art-col.is-compact { gap: var(--space-hairline, 0.14rem); }
.setting-art-col.is-dense { gap: var(--space-hairline, 0.07rem); }
.setting-art-col.is-auto { gap: var(--space-hairline, 0.14rem); }
.setting-art-col.is-dense .setting-art-row { height: 0.2rem; }
.setting-art-col.is-auto .setting-art-row:nth-child(n + 4) { height: 0.2rem; }

.setting-art-frame--density .setting-art-col {
    width: 2.6rem;
}

/* Spacing: three category blocks, moved apart. */
.setting-art-stack {
    display: flex;
    flex-direction: column;
    width: 2.8rem;
}

.setting-art-stack.is-snug { gap: var(--space-hairline, 0.12rem); }
.setting-art-stack.is-balanced { gap: var(--space-1, 0.3rem); }
.setting-art-stack.is-airy { gap: var(--space-2, 0.5rem); }

.setting-art-band {
    height: 0.34rem;
    border-radius: var(--radius-1, 0.12rem);
    background: color-mix(in srgb, var(--accent-primary) 28%, var(--border-primary));
}

/* Margins: the page, with the edges the setting leaves empty. */
.setting-art-page {
    display: flex;
    align-items: stretch;
    width: 5.2rem;
}

.setting-art-gutter {
    flex: 0 0 auto;
    background: repeating-linear-gradient(
        45deg,
        color-mix(in srgb, var(--text-tertiary) 22%, transparent) 0 2px,
        transparent 2px 4px
    );
    border-radius: var(--radius-1, 0.12rem);
}

.setting-art-page.is-snug .setting-art-gutter { width: 0.18rem; }
.setting-art-page.is-balanced .setting-art-gutter { width: 0.42rem; }
.setting-art-page.is-airy .setting-art-gutter { width: 0.75rem; }

.setting-art-content {
    display: flex;
    flex: 1 1 auto;
    gap: var(--space-hairline, 0.18rem);
    padding: 0 var(--space-hairline, 0.16rem);
}

.setting-art-content .setting-art-col {
    width: auto;
    flex: 1 1 0;
}

/* Type size: the only setting here whose subject is the letterform itself. */
.setting-art-type {
    display: inline-flex;
    align-items: baseline;
    justify-content: center;
    min-width: 1.9rem;
    color: var(--text-secondary);
    font-weight: var(--font-weight-semibold);
    line-height: 1;
}

/* All seven steps the picker offers, drawn at their own proportions rather
   than at three sizes standing in for seven. */
.setting-art-type.is-xs { font-size: var(--text-2xs, 0.6rem); }
.setting-art-type.is-s { font-size: var(--text-xs, 0.72rem); }
.setting-art-type.is-sm { font-size: var(--text-sm, 0.85rem); }
.setting-art-type.is-m { font-size: var(--text-md, 0.98rem); }
.setting-art-type.is-lg { font-size: var(--text-lg, 1.15rem); }
.setting-art-type.is-l { font-size: var(--text-xl, 1.32rem); }
.setting-art-type.is-xl { font-size: 1.55rem; }

/* A route rather than a shape: where a pasted link ends up, what a promote
   leaves behind. */
.setting-art-flow {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1, 0.3rem);
    flex-wrap: wrap;
}

.setting-art-chip {
    padding: var(--space-hairline, 0.1rem) var(--space-1-5, 0.4rem);
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-pill, 999px);
    background: var(--background-secondary);
    font-size: var(--text-xs, 0.7rem);
    color: var(--text-secondary);
    white-space: nowrap;
}

.setting-art-arrow {
    font-size: var(--text-xs, 0.75rem);
    color: var(--text-tertiary);
}

/* ─── Where the drawings sit ───────────────────────────────────────────────
   On a choice card the art leads, above the title. Beside an ordinary control
   it sits on the row's right, where the reset and info buttons are, so the
   column of labels down the left is not disturbed. */
.config-choice-card .setting-art {
    margin-bottom: var(--space-1-5, 0.35rem);
}

.config-field-art {
    display: flex;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    margin: -0.15rem 0 var(--space-2, 0.5rem) auto;
    padding-right: var(--space-hairline, 0.1rem);
}

/* In help, the drawing is the panel's opening line. */
.config-help-art {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3, 0.75rem);
    margin: 0 0 var(--space-3, 0.7rem);
    padding: var(--space-2, 0.6rem) var(--space-3, 0.7rem);
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-4, 0.4rem);
    background: var(--background-secondary);
}

.config-help-art .setting-art {
    gap: var(--space-1-5, 0.35rem);
}

.config-help-art-caption {
    font-size: var(--text-xs, 0.72rem);
    color: var(--text-tertiary);
}

@media (max-width: 640px) {
    .config-field-art {
        margin-left: 0;
    }
}

/* A choice button that leads with its drawing: the letters above, the name of
   the size under them. */
.config-choice--art {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-hairline, 0.15rem);
    padding: var(--space-1-5, 0.35rem) var(--space-2, 0.6rem);
    line-height: 1.1;
}

.config-choice--art .setting-art {
    min-height: 1.6rem;
}

.config-choice--art .config-choice-label {
    font-size: var(--text-xs, 0.72rem);
}

/* Button bar: the page, with the bar where the setting puts it. */
.setting-art-screen {
    position: relative;
    display: block;
    width: 4.4rem;
    height: 2.5rem;
    padding: var(--space-1, 0.22rem);
    border-radius: var(--radius-2, 0.2rem);
    background: var(--background-secondary);
}

.setting-art-screen-body {
    display: flex;
    gap: var(--space-hairline, 0.16rem);
    height: 100%;
}

.setting-art-screen-body .setting-art-col {
    width: auto;
    flex: 1 1 0;
}

/* The bar really does float over the grid, so the overlap is honest — but the
   bookmarks behind it are backdrop here, not the subject, and at full strength
   they compete with the one element the drawing is about. */
.setting-art-screen-body .setting-art-row {
    background: color-mix(in srgb, var(--text-tertiary) 30%, transparent);
}

.setting-art-bar {
    position: absolute;
    border-radius: var(--radius-pill, 999px);
    background: var(--accent-primary);
}

/* Horizontal bar along the bottom; the three positions differ in where along
   that edge it sits. */
.setting-art-screen.is-center-bottom .setting-art-bar,
.setting-art-screen.is-bottom-left .setting-art-bar,
.setting-art-screen.is-bottom-right .setting-art-bar {
    bottom: 0.18rem;
    height: 0.22rem;
    width: 1.5rem;
}

.setting-art-screen.is-center-bottom .setting-art-bar {
    left: 50%;
    transform: translateX(-50%);
}

.setting-art-screen.is-bottom-left .setting-art-bar { left: 0.2rem; }
.setting-art-screen.is-bottom-right .setting-art-bar { right: 0.2rem; }

/* The rails stand the same bar on its end against one edge. */
.setting-art-screen.is-side-left .setting-art-bar,
.setting-art-screen.is-side-right .setting-art-bar {
    top: 50%;
    transform: translateY(-50%);
    width: 0.22rem;
    height: 1.5rem;
}

.setting-art-screen.is-side-left .setting-art-bar { left: 0.18rem; }
.setting-art-screen.is-side-right .setting-art-bar { right: 0.18rem; }

/* Two endings from one arrow: "ask each time" does not end anywhere single. */
.setting-art-branch {
    display: inline-flex;
    flex-direction: column;
    gap: var(--space-hairline, 0.15rem);
}

/* ─── The drawings help opens with ─────────────────────────────────────────
   Help is prose about a program, and half of what it describes is a shape, a
   route, a state or a key. These are the vocabulary for the other half: the
   same blank-bar grammar as above, extended so every help panel can open with
   the picture its own subject has, rather than with a paragraph that has to be
   decoded into one. Same rules as the rest of this file — markup and CSS only,
   theme tokens throughout, aria-hidden because the sentence beside each one
   already says it. */

/* Steps: the number carries the order, which is the whole claim. */
.setting-art-steps {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1-5, 0.35rem);
    flex-wrap: wrap;
}

.setting-art-step {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1, 0.25rem);
}

.setting-art-step-n {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 0.95rem;
    height: 0.95rem;
    border-radius: var(--radius-pill, 999px);
    background: color-mix(in srgb, var(--accent-primary) 22%, var(--background-secondary));
    color: var(--text-secondary);
    font-size: var(--text-2xs, 0.6rem);
    font-weight: var(--font-weight-semibold);
    line-height: 1;
}

.setting-art-step-label {
    font-size: var(--text-xs, 0.7rem);
    color: var(--text-secondary);
    white-space: nowrap;
}

/* Keycaps, drawn the way the cheat sheet draws them so a key looks like a key
   wherever nextDash prints one. */
.setting-art-keys {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1, 0.3rem);
    flex-wrap: wrap;
}

.setting-art-key {
    min-width: 1.05rem;
    padding: var(--space-hairline, 0.08rem) var(--space-1-5, 0.32rem);
    border: 1px solid var(--border-primary);
    border-bottom-width: 2px;
    border-radius: var(--radius-3, 0.25rem);
    background: var(--background-primary);
    color: var(--text-secondary);
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: var(--text-2xs, 0.68rem);
    line-height: 1.25;
    text-align: center;
    white-space: nowrap;
}

/* A legend, in the colours the rows themselves wear. */
.setting-art-states {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    flex-wrap: wrap;
}

.setting-art-state {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1, 0.25rem);
}

.setting-art-state-label {
    font-size: var(--text-xs, 0.7rem);
    color: var(--text-secondary);
    white-space: nowrap;
}

.setting-art-dot {
    width: 0.45rem;
    height: 0.45rem;
    border-radius: var(--radius-pill, 999px);
    background: var(--text-tertiary);
    flex: 0 0 auto;
}

.setting-art-dot.is-ok { background: var(--accent-success); }
.setting-art-dot.is-warn { background: var(--accent-warning); }
.setting-art-dot.is-bad { background: var(--accent-error); }
.setting-art-dot.is-idle { background: color-mix(in srgb, var(--accent-primary) 55%, transparent); }
/* Off is drawn as a ring rather than a pale dot: nothing is being reported,
   which is not the same as a reading that happens to be quiet. */
.setting-art-dot.is-off {
    background: transparent;
    box-shadow: inset 0 0 0 1px var(--text-tertiary);
}

/* The overlay with something typed in it. */
.setting-art-frame--field { padding: var(--space-1, 0.22rem) var(--space-1-5, 0.35rem); }

.setting-art-field {
    display: inline-flex;
    align-items: center;
    gap: var(--space-hairline, 0.15rem);
    min-width: 6.5rem;
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: var(--text-xs, 0.72rem);
    line-height: 1.3;
}

.setting-art-q.is-prefix {
    padding: 0 var(--space-1, 0.2rem);
    border-radius: var(--radius-2, 0.2rem);
    background: color-mix(in srgb, var(--accent-primary) 28%, transparent);
    color: var(--text-primary);
    font-weight: var(--font-weight-semibold);
}

.setting-art-q.is-token {
    padding: 0 var(--space-1, 0.2rem);
    border-radius: var(--radius-2, 0.2rem);
    background: color-mix(in srgb, var(--accent-primary) 14%, transparent);
    color: var(--text-secondary);
}

.setting-art-q.is-text { color: var(--text-secondary); }

.setting-art-caret {
    width: 1px;
    height: 0.8rem;
    margin-left: var(--space-hairline, 0.1rem);
    background: var(--accent-primary);
}

/* Counts, with the one being talked about picked out. */
.setting-art-frame--bars { align-items: flex-end; }

.setting-art-bars {
    display: flex;
    align-items: flex-end;
    gap: var(--space-hairline, 0.18rem);
    height: 1.5rem;
}

.setting-art-bar-col {
    width: 0.32rem;
    border-radius: 0.1rem 0.1rem 0 0;
    background: color-mix(in srgb, var(--accent-primary) 30%, var(--border-primary));
}

.setting-art-bar-col.is-mark { background: var(--accent-primary); }
.setting-art-bar-col.is-mark.is-bad { background: var(--accent-error); }
.setting-art-bar-col.is-mark.is-warn { background: var(--accent-warning); }

/* A trend, drawn as the line the real row draws. */
.setting-art-spark {
    display: block;
    width: 3.4rem;
    height: 1.2rem;
    color: var(--accent-primary);
    overflow: visible;
}

/* How far along, and where it starts to matter. */
.setting-art-meter {
    position: relative;
    display: block;
    width: 4.2rem;
    height: 0.45rem;
    border-radius: var(--radius-pill, 999px);
    background: color-mix(in srgb, var(--text-tertiary) 22%, transparent);
    overflow: hidden;
}

.setting-art-meter-fill {
    position: absolute;
    inset: 0 auto 0 0;
    border-radius: var(--radius-pill, 999px);
    background: var(--accent-primary);
}

.setting-art-meter-fill.is-warn { background: var(--accent-warning); }
.setting-art-meter-fill.is-bad { background: var(--accent-error); }

.setting-art-meter-mark {
    position: absolute;
    top: -0.1rem;
    bottom: -0.1rem;
    width: 1px;
    background: var(--text-secondary);
}

/* A day, with the hours you expect to be down shaded. */
.setting-art-day {
    display: flex;
    gap: 1px;
}

.setting-art-hour {
    width: 0.11rem;
    height: 0.85rem;
    border-radius: var(--radius-1, 0.05rem);
    background: color-mix(in srgb, var(--text-tertiary) 25%, transparent);
}

.setting-art-hour.is-window {
    background: var(--accent-warning);
}

/* Switches: some on, some off, which is what a settings tab looks like. */
.setting-art-toggles {
    display: flex;
    flex-direction: column;
    gap: var(--space-1, 0.2rem);
}

.setting-art-toggle {
    position: relative;
    display: block;
    width: 1.35rem;
    height: 0.55rem;
    border-radius: var(--radius-pill, 999px);
    background: color-mix(in srgb, var(--text-tertiary) 28%, transparent);
}

.setting-art-toggle.is-on {
    background: color-mix(in srgb, var(--accent-primary) 70%, transparent);
}

.setting-art-knob {
    position: absolute;
    top: 0.08rem;
    left: 0.08rem;
    width: 0.39rem;
    height: 0.39rem;
    border-radius: var(--radius-pill, 999px);
    background: var(--background-primary);
}

.setting-art-toggle.is-on .setting-art-knob {
    left: auto;
    right: 0.08rem;
}

/* Colour plates. Light and dark are halves of one theme, so they are drawn
   touching rather than as two separate chips. */
.setting-art-swatches {
    display: inline-flex;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-3, 0.3rem);
    overflow: hidden;
}

.setting-art-swatch {
    width: 1.1rem;
    height: 1.1rem;
}

/* The theme's own ground pushed to either end, so both halves stay light and
   dark in any theme but still belong to the one they sit in. */
.setting-art-swatch.is-light { background: color-mix(in srgb, var(--background-primary) 8%, white); }
.setting-art-swatch.is-dark { background: color-mix(in srgb, var(--background-primary) 12%, black); }
.setting-art-swatch.is-accent { background: var(--accent-primary); }

/* The line your data does not cross. */
.setting-art-boundary-wrap {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1-5, 0.35rem);
}

.setting-art-boundary {
    display: inline-flex;
    flex-direction: column;
    gap: var(--space-1, 0.2rem);
    padding: var(--space-1, 0.25rem) var(--space-1-5, 0.35rem);
    border: 1px dashed var(--border-primary);
    border-radius: var(--radius-4, 0.35rem);
    background: var(--background-primary);
}

.setting-art-boundary-label {
    font-size: var(--text-2xs, 0.62rem);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-tertiary);
}

.setting-art-boundary-body {
    display: inline-flex;
    gap: var(--space-1, 0.25rem);
    flex-wrap: wrap;
}

.setting-art-blocked {
    font-size: var(--text-sm, 0.85rem);
    line-height: 1;
    color: var(--accent-error);
}

/* One row, with the parts it can carry. */
.setting-art-bmrow {
    display: flex;
    align-items: center;
    gap: var(--space-1, 0.25rem);
    width: 8rem;
}

.setting-art-bm-icon {
    width: 0.5rem;
    height: 0.5rem;
    border-radius: var(--radius-1, 0.12rem);
    background: color-mix(in srgb, var(--accent-primary) 45%, transparent);
    flex: 0 0 auto;
}

.setting-art-bm-name {
    flex: 1 1 auto;
    height: 0.28rem;
    border-radius: var(--radius-1, 0.12rem);
    background: color-mix(in srgb, var(--accent-primary) 30%, var(--border-primary));
}

.setting-art-bm-tag {
    width: 0.85rem;
    height: 0.35rem;
    border-radius: var(--radius-pill, 999px);
    background: color-mix(in srgb, var(--accent-primary) 18%, var(--border-primary));
    flex: 0 0 auto;
}

/* Config drawn as itself: rail, tabs, panel — the three places a sentence
   about "where the setting is" can mean. */
.setting-art-config {
    display: flex;
    gap: var(--space-1, 0.2rem);
    width: 4.6rem;
    height: 2.2rem;
}

.setting-art-rail {
    width: 0.6rem;
    border-radius: var(--radius-1, 0.15rem);
    background: color-mix(in srgb, var(--text-tertiary) 22%, transparent);
}

.setting-art-config-main {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    gap: var(--space-1, 0.2rem);
}

.setting-art-tabs {
    height: 0.3rem;
    border-radius: var(--radius-1, 0.15rem);
    background: color-mix(in srgb, var(--text-tertiary) 22%, transparent);
}

.setting-art-config-body {
    display: flex;
    flex: 1 1 auto;
    gap: var(--space-1, 0.2rem);
    padding: var(--space-hairline, 0.12rem);
    border-radius: var(--radius-1, 0.15rem);
    background: color-mix(in srgb, var(--text-tertiary) 12%, transparent);
}

.setting-art-config-body .setting-art-col { width: auto; flex: 1 1 0; }

.setting-art-rail.is-active,
.setting-art-tabs.is-active { background: var(--accent-primary); }

.setting-art-config-body.is-active {
    background: color-mix(in srgb, var(--accent-primary) 18%, transparent);
}

/* Help stacks several drawings per panel now, so the strip wraps rather than
   running off the panel on a narrow window, and each drawing keeps its caption
   under it rather than beside the next drawing's. */
.config-help-art-item {
    display: inline-flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-1, 0.2rem);
}

/* ─── v1.4.0 drawings ───────────────────────────────────────────────────────
   Four subjects that arrived with the theme engine and the widgets, each one a
   relationship rather than a value: how far a card sits off the page, what
   shape the backdrop is, how wide a block is beside its neighbours, and what a
   grid of themes looks like next to a list of names. */

/* Depth: the same card at three distances from the page behind it.

   Drawn structurally rather than tonally. The first attempt used a tint, a soft
   shadow and a wash — all three real, all three invisible at 3.4rem on a dark
   theme, so flat, soft and rich came out as three identical squares. A drawing
   that cannot be told apart is worse than no drawing: it says the settings do
   not differ.

   So the elevation is a plate offset behind the card, which reads at any size
   and in any palette: none at flat, one at soft, two at rich. */
.setting-art-frame--depth { padding: 0; }

.setting-art-depth {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 3.4rem;
    height: 2.2rem;
    border-radius: var(--radius-3, 0.3rem);
    overflow: hidden;
    background: var(--background-primary);
    /* Its own stacking context, so the plates below can sit behind the card
       without falling behind the panel the whole drawing is painted on. */
    isolation: isolate;
}
.setting-art-depth.is-soft { background: color-mix(in srgb, var(--accent-primary) 10%, var(--background-primary)); }
.setting-art-depth.is-rich { background: color-mix(in srgb, var(--accent-primary) 20%, var(--background-primary)); }

/* The wash behind the page, which only rich really has. */
.setting-art-depth-wash {
    position: absolute;
    inset: -50% -30% auto -30%;
    height: 150%;
    background: radial-gradient(
        closest-side,
        color-mix(in srgb, var(--accent-primary) 55%, transparent),
        transparent 70%);
    opacity: 0;
}
.setting-art-depth.is-soft .setting-art-depth-wash { opacity: 0.35; }
.setting-art-depth.is-rich .setting-art-depth-wash { opacity: 0.85; }

.setting-art-depth-card {
    position: relative;
    z-index: 1;
    display: flex;
    gap: var(--space-hairline, 0.18rem);
    padding: var(--space-1, 0.28rem) var(--space-1-5, 0.32rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-3, 0.22rem);
    background: var(--background-secondary);
}

/* The plates. ::before is the near one, ::after the far one — so soft shows a
   single step off the page and rich shows a stack. */
.setting-art-depth-card::before,
.setting-art-depth-card::after {
    content: "";
    position: absolute;
    inset: 0;
    border: 1px solid color-mix(in srgb, var(--text-secondary) 55%, transparent);
    border-radius: inherit;
    background: var(--background-secondary);
    /* Behind the card, in front of the page: zero rather than a negative,
       which would leave the drawing's own container. */
    z-index: 0;
    opacity: 0;
}
.setting-art-depth-card::before { transform: translate(3px, 3px); }
.setting-art-depth-card::after { transform: translate(6px, 6px); }

/* The rows sit above the plates: a positioned pseudo paints over ordinary
   content, so without this the card's own contents disappear the moment it is
   given any elevation at all. */
.setting-art-depth-card .setting-art-col {
    position: relative;
    z-index: 2;
}

.setting-art-depth.is-flat .setting-art-depth-card {
    background: var(--background-primary);
}
.setting-art-depth.is-soft .setting-art-depth-card::before { opacity: 1; }
.setting-art-depth.is-rich .setting-art-depth-card::before,
.setting-art-depth.is-rich .setting-art-depth-card::after { opacity: 1; }

/* The backdrop, in each of its five shapes. */
.setting-art-frame--pattern { padding: 0; }

.setting-art-pattern {
    display: block;
    width: 3.4rem;
    height: 2.2rem;
    border-radius: var(--radius-3, 0.3rem);
    background-color: var(--background-primary);
    --art-ink: color-mix(in srgb, var(--text-secondary) 45%, transparent);
}
.setting-art-pattern.is-dots {
    background-image: radial-gradient(var(--art-ink) 1px, transparent 1px);
    background-size: 6px 6px;
}
.setting-art-pattern.is-grid {
    background-image:
        linear-gradient(var(--art-ink) 1px, transparent 1px),
        linear-gradient(90deg, var(--art-ink) 1px, transparent 1px);
    background-size: 7px 7px;
}
.setting-art-pattern.is-lines {
    background-image: linear-gradient(var(--art-ink) 1px, transparent 1px);
    background-size: 100% 6px;
}
.setting-art-pattern.is-hatch {
    background-image: repeating-linear-gradient(
        45deg, var(--art-ink) 0 1px, transparent 1px 6px);
}
.setting-art-pattern.is-auto {
    background-image: radial-gradient(var(--art-ink) 1px, transparent 1px);
    background-size: 6px 6px;
    opacity: 0.6;
}
/* "None" is drawn as the empty frame it is, not as a missing picture. */
.setting-art-pattern.is-none { background-image: none; }

/* A widget beside its neighbours, at one column or two. */
.setting-art-blocks {
    display: flex;
    align-items: flex-start;
    gap: var(--space-1, 0.25rem);
}

.setting-art-block {
    display: flex;
    flex-direction: column;
    gap: var(--space-1, 0.2rem);
    width: 0.85rem;
    padding: var(--space-1, 0.2rem);
    border: 1px solid color-mix(in srgb, var(--accent-primary) 45%, transparent);
    border-radius: var(--radius-2, 0.2rem);
    background: color-mix(in srgb, var(--accent-primary) 10%, transparent);
}
.setting-art-block.is-wide { width: 1.95rem; }
.setting-art-block .setting-art-band {
    height: 3px;
    border-radius: var(--radius-1, 1px);
    background: color-mix(in srgb, var(--accent-primary) 55%, transparent);
}

/* Cards of themes, which is what replaced a list of their names. */
.setting-art-themes {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: var(--space-1, 0.2rem);
}

.setting-art-theme {
    display: flex;
    align-items: flex-end;
    width: 0.95rem;
    height: 0.7rem;
    padding: var(--space-hairline, 0.12rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-1, 0.15rem);
}
.setting-art-theme-bar {
    width: 100%;
    height: 2px;
    border-radius: var(--radius-1, 1px);
}
/* Six plates that differ from one another rather than naming real themes: the
   argument is that you can see them, not which ones they are. */
.setting-art-theme.is-a { background: #15171c; }
.setting-art-theme.is-a .setting-art-theme-bar { background: #7aa2f7; }
.setting-art-theme.is-b { background: #faf4ed; }
.setting-art-theme.is-b .setting-art-theme-bar { background: #b4637a; }
.setting-art-theme.is-c { background: #1b1e28; }
.setting-art-theme.is-c .setting-art-theme-bar { background: #a6e3a1; }
.setting-art-theme.is-d { background: #fdf6e3; }
.setting-art-theme.is-d .setting-art-theme-bar { background: #cb4b16; }
.setting-art-theme.is-e { background: #101a28; }
.setting-art-theme.is-e .setting-art-theme-bar { background: #f5c2e7; }
.setting-art-theme.is-f { background: #eef2f7; }
.setting-art-theme.is-f .setting-art-theme-bar { background: #2f6f9f; }

/* ─── A block being moved (help: moving categories and widgets) ──────────────
   Three short loops on a stand-in grid: a block lifted from one column and set
   down in another with the dashed landing box ahead of it, a block dropped on
   the line between two columns so it spans both, and a block sent to another
   page. Every part rests at the end of the move, so without animation the
   drawing still shows where the block went. */
.setting-art-frame--move { padding: 0; }

.setting-art-move {
    --col: 1.7rem;
    --gap: 0.35rem;
    --pad: 0.35rem;
    position: relative;
    display: block;
    width: calc(var(--col) * 3 + var(--gap) * 2 + var(--pad) * 2);
    height: 4.1rem;
}
.setting-art-move.is-wide { width: calc(var(--col) * 2 + var(--gap) + var(--pad) * 2); }

.setting-art-move-block,
.setting-art-move-landing {
    position: absolute;
    box-sizing: border-box;
    width: var(--col);
    height: 1.35rem;
    border-radius: var(--radius-2, 0.2rem);
}

.setting-art-move-block {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    padding: 0.25rem;
    border: 1px solid var(--border-primary);
    background: color-mix(in srgb, var(--text-primary) 6%, transparent);
}
.setting-art-move-block .setting-art-band {
    height: 3px;
    border-radius: var(--radius-1, 1px);
    background: color-mix(in srgb, var(--text-primary) 22%, transparent);
}
.setting-art-move-block .setting-art-band:nth-child(2) { width: 70%; }

/* Columns: a = left, b = middle, c = right; 1 = top row, 2 = the row under it. */
.setting-art-move .is-a1 { left: var(--pad); top: var(--pad); }
.setting-art-move .is-a2 { left: var(--pad); top: calc(var(--pad) + 1.6rem); }
.setting-art-move .is-b1 { left: calc(var(--pad) + var(--col) + var(--gap)); top: var(--pad); }
.setting-art-move .is-c1 { left: calc(var(--pad) + (var(--col) + var(--gap)) * 2); top: var(--pad); }
.setting-art-move .is-c2 { left: calc(var(--pad) + (var(--col) + var(--gap)) * 2); top: calc(var(--pad) + 1.6rem); }

.setting-art-move-block.is-moving {
    border-color: color-mix(in srgb, var(--accent-primary) 60%, transparent);
    background: color-mix(in srgb, var(--accent-primary) 18%, var(--background-primary));
    z-index: 2;
}
.setting-art-move-block.is-moving .setting-art-band {
    background: color-mix(in srgb, var(--accent-primary) 65%, transparent);
}

.setting-art-move-landing {
    border: 1.5px dashed color-mix(in srgb, var(--accent-primary) 75%, transparent);
    opacity: 0;
}

/* place: from the left column's second row to the middle column, under its block. */
.setting-art-move.is-place .is-moving,
.setting-art-move.is-place .setting-art-move-landing {
    left: calc(var(--pad) + var(--col) + var(--gap));
    top: calc(var(--pad) + 1.6rem);
}
.setting-art-move.is-place .is-moving { animation: setting-art-move-place 4.8s ease-in-out infinite both; }
.setting-art-move.is-place .setting-art-move-landing { animation: setting-art-move-landing 4.8s ease-in-out infinite both; }

@keyframes setting-art-move-place {
    0%, 14% { transform: translate(calc((var(--col) + var(--gap)) * -1), 0); box-shadow: none; }
    22% { transform: translate(calc((var(--col) + var(--gap)) * -1), -2px) scale(1.06); box-shadow: 0 3px 8px rgba(0, 0, 0, 0.35); }
    58% { transform: translate(0, -2px) scale(1.06); box-shadow: 0 3px 8px rgba(0, 0, 0, 0.35); }
    66%, 100% { transform: none; box-shadow: none; }
}

@keyframes setting-art-move-landing {
    0%, 18% { opacity: 0; }
    26%, 60% { opacity: 1; }
    66%, 100% { opacity: 0; }
}

/* wide: dropped on the line between the two columns, it spans both. */
.setting-art-move-seam {
    position: absolute;
    left: calc(var(--pad) + var(--col) + var(--gap) / 2 - 1px);
    top: var(--pad);
    bottom: var(--pad);
    width: 2px;
    border-radius: 1px;
    background: var(--accent-primary);
    opacity: 0;
    animation: setting-art-move-seam 4.8s ease-in-out infinite both;
}

.setting-art-move.is-wide .is-moving,
.setting-art-move.is-wide .setting-art-move-landing {
    left: var(--pad);
    top: calc(var(--pad) + 1.6rem);
    width: calc(var(--col) * 2 + var(--gap));
}
.setting-art-move.is-wide .is-moving { animation: setting-art-move-wide 4.8s ease-in-out infinite both; }
.setting-art-move.is-wide .setting-art-move-landing { animation: setting-art-move-landing 4.8s ease-in-out infinite both; }

@keyframes setting-art-move-wide {
    0%, 14% { width: var(--col); transform: translate(calc(var(--col) + var(--gap)), 0); }
    22% { width: var(--col); transform: translate(calc(var(--col) + var(--gap)), -2px) scale(1.06); }
    50% { width: var(--col); transform: translate(calc(var(--col) / 2), -2px) scale(1.06); }
    62%, 100% { width: calc(var(--col) * 2 + var(--gap)); transform: none; }
}

@keyframes setting-art-move-seam {
    0%, 34% { opacity: 0; }
    42%, 58% { opacity: 1; }
    66%, 100% { opacity: 0; }
}

/* page: two page tabs over the grid; the block goes into tab 2. */
.setting-art-move.is-page { padding-top: 0.9rem; height: 5rem; }
.setting-art-move.is-page .setting-art-move-block:not(.is-moving),
.setting-art-move.is-page .is-moving { margin-top: 0.9rem; }

.setting-art-move-tabs {
    position: absolute;
    top: var(--pad);
    right: var(--pad);
    display: flex;
    gap: 0.25rem;
}

.setting-art-move-tab {
    min-width: 0.85rem;
    padding: 0 0.15rem;
    border-radius: var(--radius-1, 0.15rem);
    font-size: 0.62rem;
    line-height: 0.95rem;
    text-align: center;
    color: var(--text-tertiary);
    border: 1px solid var(--border-primary);
}
.setting-art-move-tab.is-here { color: var(--text-primary); border-color: color-mix(in srgb, var(--text-primary) 40%, transparent); }
.setting-art-move-tab.is-target {
    color: var(--accent-primary);
    border-color: var(--accent-primary);
    animation: setting-art-move-tab 4.8s ease-in-out infinite both;
}

.setting-art-move.is-page .is-moving {
    left: calc(var(--pad) + var(--col) + var(--gap));
    top: calc(var(--pad) + 1.6rem);
    opacity: 0;
    animation: setting-art-move-page 4.8s ease-in-out infinite both;
}
.setting-art-move.is-page .setting-art-move-landing { display: none; }

@keyframes setting-art-move-page {
    0%, 16% { opacity: 1; transform: none; }
    24% { opacity: 1; transform: translateY(-2px) scale(1.06); }
    56% { opacity: 1; transform: translate(calc(var(--col) * 1.15), -3rem) scale(0.35); }
    64%, 100% { opacity: 0; transform: translate(calc(var(--col) * 1.15), -3rem) scale(0.2); }
}

@keyframes setting-art-move-tab {
    0%, 56% { transform: none; background: transparent; }
    62% { transform: scale(1.3); background: color-mix(in srgb, var(--accent-primary) 25%, transparent); }
    70%, 100% { transform: none; background: color-mix(in srgb, var(--accent-primary) 18%, transparent); }
}

/* The action bar's intro (actionBarIntroStyle): a bar of six buttons, at rest
   until its card has the pointer or the focus; the animation is dashboard.css's. */
.setting-art-frame--intro {
    padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
}

.setting-art-intro {
    display: flex;
    gap: 0.2rem;
    padding: 0.2rem;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-3, 0.25rem);
}

.setting-art-intro-button {
    width: 0.8rem;
    height: 0.8rem;
    border-radius: var(--radius-2, 0.18rem);
    background: color-mix(in srgb, var(--accent-primary) 45%, var(--border-primary));
}

/* ==== css/dashboard-multi-select.css ==== */
/* Multi-select on the bookmark grid.
 *
 * Deliberately built from the same tokens as .inbox-selection-bar: the two are
 * the same idea in two views, and a second visual language for "these rows are
 * ticked" would read as a different feature.
 */

.multi-select-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2, 0.5rem);
    width: 100%;
    margin-bottom: var(--space-3, 0.75rem);
    padding: var(--space-2, 0.45rem) var(--space-3, 0.65rem);
    border-radius: var(--layout-radius-sm, 8px);
    border: 1px solid color-mix(in srgb, var(--accent-primary) 40%, transparent);
    background: color-mix(in srgb, var(--accent-primary) 8%, var(--background-secondary));
    /* The grid scrolls under it; the toolbar acts on a selection that may sit
     * off-screen, so it has to stay reachable. Same sticky band as
     * list-view-shell.css's .lvs-header (layer-tokens.css) — the two never
     * need to out-rank each other, only the row list beneath both of them. */
    position: sticky;
    top: 0;
    z-index: var(--layer-sticky-band, 5);
}

.multi-select-count {
    margin-right: var(--space-1, 0.25rem);
    font-size: var(--text-sm, 0.8rem);
    font-weight: 600;
    color: var(--accent-primary);
    font-variant-numeric: tabular-nums;
}

.multi-select-btn {
    padding: var(--space-1, 0.3rem) var(--space-2, 0.6rem);
    font-size: var(--text-sm, 0.8rem);
    font-family: inherit;
    color: var(--text-primary);
    background: var(--background-primary);
    border: 1px solid color-mix(in srgb, var(--text-primary) 18%, transparent);
    border-radius: var(--layout-radius-sm, 6px);
    cursor: pointer;
}

.multi-select-btn:hover {
    border-color: color-mix(in srgb, var(--accent-primary) 55%, transparent);
    color: var(--accent-primary);
}

.multi-select-btn:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
}

.multi-select-btn.danger:hover {
    border-color: color-mix(in srgb, var(--accent-error) 55%, transparent);
    color: var(--accent-error);
}

/* Ticked rows.
 *
 * An inset ring rather than a background swap, so it stacks with the keyboard
 * cursor and the status accent edge instead of competing with them: a row can
 * be selected, keyboard-focused and offline at the same time.
 */
.bookmark-link.is-multi-selected {
    box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent-primary) 60%, transparent);
    background: color-mix(in srgb, var(--accent-primary) 10%, transparent);
}

/* Rounded, in the theme's own radius.
 *
 * A row only takes its rounding on hover or under the cursor, so a ticked row
 * at rest drew its ring with square corners -- the one shape nothing else in
 * the app uses. The grouped and tag-filter lists (the kept tab among them) go
 * further and pin their rows square, which is why this names them: the
 * selection has to win there too. --radius-5 is scaled by the theme's own
 * radius setting (theme-character.css), so a square theme stays square.
 */
.bookmark-link.is-multi-selected,
#dashboard-layout.tag-filter-view .tag-filter-chunk .bookmark-link.is-multi-selected {
    border-radius: var(--radius-5, 8px);
}

.bookmark-link.is-multi-selected.keyboard-selected {
    box-shadow: inset 0 0 0 2px var(--accent-primary);
}

/* While a selection is open, a plain click clears it rather than opening the
 * link, so the pointer should not promise navigation. */
body.has-multi-selection .bookmark-link a.bookmark-open {
    cursor: default;
}

@media (prefers-reduced-motion: reduce) {
    .bookmark-link.is-multi-selected {
        transition: none;
    }
}

/* ==== css/view-explainers.css ==== */
/* ─── Explaining a view ──────────────────────────────────────────────────────

   The health and inbox views both explain themselves the same way: a sentence
   under the toolbar saying what the active filter selects, and an ℹ button that
   opens the longer "how this works" modal. Both were written for health first;
   this is that styling with the view prefix taken off, so the second view does
   not carry a near-verbatim copy that drifts from the first.

   View-specific stylesheets keep their own `.health-view-*` / `.inbox-*` aliases
   pointing at these rules, so existing markup and tests keep working.
   ────────────────────────────────────────────────────────────────────────── */

/* One sentence about the active filter, under the toolbar. Quiet enough to be
   skipped once it is known, present enough to be read the first time. */
.view-filter-note {
    margin: -0.2rem 0 var(--space-3, 0.7rem);
    padding: 0 var(--space-1-5, 0.35rem);
    font-size: var(--text-xs, 0.74rem);
    line-height: 1.5;
    color: var(--text-secondary);
    max-width: 70ch;
}

/* The ℹ in the toolbar. Pushed to the far end so it never sits between two
   actions and gets hit by accident. */
.view-help-btn {
    margin-left: auto;
    flex: 0 0 auto;
    width: 1.6rem;
    height: 1.6rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-4, 0.4rem);
    background: transparent;
    color: var(--text-secondary);
    font-size: var(--text-sm, 0.78rem);
    line-height: 1;
    cursor: pointer;
}

.view-help-btn:hover,
.view-help-btn:focus-visible {
    border-color: var(--accent-primary);
    color: var(--text-primary);
}

/* The explainer modal.

   Without a max-height of its own the panel grows to whatever its sections need
   — health's six were measured at 786px in an 800px viewport, so it touched the
   bottom edge and left 14px at the top. Capping it and letting the body scroll
   keeps a clear margin above and below at any window height, and the caps are
   expressed against both vh and a fixed inset so a short window still gets its
   gap. */
.modal.view-explain-modal {
    max-height: min(78vh, calc(100vh - 6rem));
    overflow: hidden;
    display: flex;
    flex-direction: column;
}

.modal.view-explain-modal .modal-body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
}

/* Config renders its own overlay above this one, where the viewport is already
   framed by the config chrome; the tighter cap keeps the same visual margin. */
body:has(#config-main) #app-modal.modal-overlay:has(.view-explain-modal) .modal {
    max-height: min(70vh, calc(100vh - 4rem));
}

/* Tighter than the 0.9rem gap the check-mode explainer uses: six sections at
   that spacing is what made health's too tall to fit. */
.view-explain-row + .view-explain-row {
    margin-top: var(--space-3, 0.65rem);
}

.view-explain-row h4 {
    margin: 0 0 var(--space-hairline, 0.1rem);
    font-size: var(--text-base, 0.88rem);
    color: var(--text-primary);
}

.view-explain-row p {
    margin: 0;
    font-size: var(--text-sm, 0.82rem);
    color: var(--text-secondary);
    line-height: 1.45;
}

/* ==== css/health-focus.css ==== */
/*
 * Focus mode: one health issue at a time, over the list it came from.
 *
 * Styled as an overlay rather than a separate page on purpose — the list stays
 * visible behind it, so working through a queue never feels like leaving the
 * view. The card is deliberately narrow: the whole point is that one decision
 * is on screen and nothing else is competing for the click.
 */

.health-focus-overlay {
    position: fixed;
    inset: 0;
    /* Above the view's sticky header (+1300) and the side panel (+1400), so
       the scrim covers them as it does behind every other modal; below the
       app modal (2000), so a confirmation from the card opens over it. */
    z-index: calc(var(--layer-sticky-band, 5) + 1500);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-5, 1.5rem);
    /* The same scrim as every other modal (modal.css). */
    background: var(--modal-scrim);
    backdrop-filter: blur(var(--modal-scrim-blur));
    -webkit-backdrop-filter: blur(var(--modal-scrim-blur));
    animation: health-focus-fade 0.14s ease-out;
}

.health-focus-card {
    /* One decision at a time: a reading column, not a toolbar. */
    width: min(34rem, 100%);
    max-height: 85vh;
    overflow-y: auto;
    padding: var(--space-5, 1.5rem);
    /* The modal's own surface (modal.css), so a theme's depth reaches it:
       glass makes it glass, as it does every other modal. */
    border: 1px solid color-mix(in srgb, var(--border-primary) 75%, transparent);
    border-radius: var(--layout-radius-md, 12px);
    background: var(--background-modal);
    color: var(--text-primary);
    box-shadow: 0 24px 64px rgba(0, 0, 0, 0.28), 0 2px 8px rgba(0, 0, 0, 0.12);
    animation: health-focus-rise 0.16s ease-out;
}

/* While an action is in flight the card stays put and stops taking clicks,
   rather than swapping in a spinner that would move everything underneath the
   pointer mid-decision. */
.health-focus-overlay.is-busy .health-focus-card {
    opacity: 0.6;
    pointer-events: none;
}

.health-focus-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4, 1rem);
    margin-bottom: var(--space-3, 0.75rem);
}

.health-focus-progress {
    font-size: var(--text-sm, 0.78rem);
    color: var(--text-secondary);
    font-variant-numeric: tabular-nums;
}

.health-focus-close {
    border: none;
    background: transparent;
    color: var(--text-secondary);
    font-size: 1.4rem;
    line-height: 1;
    padding: 0 var(--space-1, 0.25rem);
    cursor: pointer;
    border-radius: var(--radius-3, 0.3rem);
}

.health-focus-close:hover {
    color: var(--text-primary);
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
}

/* The favicon rides beside the name, the same pairing the list row uses, so a
   card is recognisable before it is read. */
.health-focus-identity {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2, 0.6rem);
    margin-bottom: var(--space-2, 0.5rem);
}

.health-focus-identity-text {
    min-width: 0;
    flex: 1;
}

.health-focus-icon {
    flex: 0 0 auto;
    width: 1.75rem;
    height: 1.75rem;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--text-md, 1rem);
    line-height: 1;
}

.health-focus-icon-img {
    width: 1.5rem;
    height: 1.5rem;
    object-fit: contain;
    border-radius: var(--radius-3, 0.25rem);
}

.health-focus-title {
    margin: 0 0 var(--space-1, 0.25rem);
    font-size: var(--text-lg, 1.15rem);
    line-height: 1.3;
    /* Long bookmark names break rather than widening the card past the
       measure the rest of the layout is built on. */
    overflow-wrap: anywhere;
}

.health-focus-url {
    margin: 0;
    font-size: var(--text-sm, 0.82rem);
    color: var(--text-secondary);
    overflow-wrap: anywhere;
}

/* The line the Open button changes. Never-opened is called out in the warning
   colour because it is one of the findings, not a neutral timestamp. */
.health-focus-opened {
    margin: 0 0 var(--space-3, 0.75rem);
    font-size: var(--text-sm, 0.78rem);
    color: var(--text-secondary);
    font-variant-numeric: tabular-nums;
}

.health-focus-opened.is-never {
    color: var(--accent-warning);
}

.health-focus-badges {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1-5, 0.35rem);
    margin-bottom: var(--space-3, 0.75rem);
}

/* Collapses to nothing when the row has no badges, so the gap above the
   reasons does not depend on whether any were rendered. */
.health-focus-badges:empty {
    display: none;
    margin-bottom: 0;
}

/* What the page says about itself. Bounded on purpose: the preview informs the
   decision, it is not the decision, so it never grows tall enough to push the
   buttons off the card. */
.health-focus-preview {
    display: flex;
    gap: var(--space-3, 0.75rem);
    align-items: flex-start;
    margin: 0 0 var(--space-4, 0.9rem);
    padding: var(--space-2, 0.55rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-5, 0.5rem);
    background: var(--background-primary);
    overflow: hidden;
}

.health-focus-preview-text {
    min-width: 0;
    flex: 1;
}

.health-focus-preview.is-loading {
    opacity: 0.6;
}

/* No preview is a line, not a box: nothing to show should not take a panel. */
.health-focus-preview.is-empty {
    padding: 0;
    border: 0;
    background: none;
}







.health-focus-preview-image {
    flex: 0 0 5.5rem;
    border-radius: var(--radius-4, 0.35rem);
    overflow: hidden;
    background: color-mix(in srgb, var(--text-primary) 5%, transparent);
}

.health-focus-preview-image img {
    display: block;
    width: 100%;
    height: 3.6rem;
    object-fit: cover;
}

.health-focus-preview-title {
    margin: 0 0 var(--space-1, 0.2rem);
    font-size: var(--text-base, 0.86rem);
    color: var(--text-primary);
    overflow-wrap: anywhere;
}

.health-focus-preview-desc {
    margin: 0;
    font-size: var(--text-sm, 0.8rem);
    line-height: 1.45;
    color: var(--text-secondary);
    /* Three lines is enough to tell what a page is about; more turns the card
       into a reading surface and buries the decision. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.health-focus-preview-empty {
    margin: 0;
    font-size: var(--text-sm, 0.78rem);
    color: var(--text-tertiary);
}


.health-focus-reasons {
    margin: 0 0 var(--space-4, 1.1rem);
    padding-left: var(--space-4, 1.1rem);
    font-size: var(--text-base, 0.86rem);
    color: var(--text-secondary);
    line-height: 1.5;
}

.health-focus-reasons li + li {
    margin-top: var(--space-1, 0.2rem);
}

/* A reason answered on this card. Struck rather than removed: the score and the
   list behind still count it until the next report, and deleting the line would
   make the card disagree with both. */
.health-focus-reasons li.is-resolved {
    text-decoration: line-through;
    opacity: 0.6;
}

.health-focus-reason-done {
    text-decoration: none;
    font-size: var(--text-xs, 0.74rem);
    color: var(--accent-success);
}

.health-focus-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2, 0.5rem);
    margin-bottom: var(--space-4, 0.9rem);
}

.health-focus-actions .config-btn {
    padding-inline: var(--space-3, 0.75rem);
    white-space: nowrap;
}

/* The key hint rides inside its button, the same pairing the row actions use,
   so the shortcut is learned from the control rather than from a legend. */
.health-focus-actions kbd {
    margin-left: var(--space-1-5, 0.4rem);
    padding: var(--space-hairline, 0.05rem) var(--space-1, 0.3rem);
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-3, 0.25rem);
    background: color-mix(in srgb, var(--text-primary) 6%, transparent);
    font-family: inherit;
    font-size: var(--text-2xs, 0.68rem);
    opacity: 0.8;
}

.health-focus-legend {
    margin: 0;
    font-size: var(--text-xs, 0.74rem);
    color: var(--text-tertiary);
}

@keyframes health-focus-fade {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes health-focus-rise {
    from { opacity: 0; transform: translateY(8px) scale(0.99); }
    to { opacity: 1; transform: translateY(0) scale(1); }
}

@media (prefers-reduced-motion: reduce) {
    .health-focus-overlay,
    .health-focus-card {
        animation: none;
    }
}

/* On a phone the card is the screen: full width, and the actions stack so no
   button ends up too narrow to hit. */
@media (max-width: 30rem) {
    .health-focus-overlay {
        padding: var(--space-3, 0.75rem);
    }

    .health-focus-card {
        width: 100%;
        padding: var(--space-4, 1.1rem);
    }

    .health-focus-actions .config-btn {
        flex: 1 1 100%;
    }
}

/* The end of a review session. Same card, no row on it — what is left to show
   is a count and a decision about tomorrow. */
.health-focus-card--done {
    text-align: center;
}

.health-focus-done-count {
    margin: var(--space-3, 0.75rem) 0 var(--space-1, 0.25rem);
    font-size: var(--text-md, 1.05rem);
    color: var(--text-primary);
}

.health-focus-done-rest {
    margin: 0 0 1.25rem;
    font-size: var(--text-sm, 0.85rem);
    color: var(--text-secondary);
}

.health-focus-card--done .health-focus-actions {
    justify-content: center;
}

.health-focus-done-primary {
    border-color: var(--accent-primary);
    color: var(--accent-primary);
}



/* ── The card, reworked: why first, one next step, the rest quiet ─────────── */

/* How far through the pile: a thin line under the count. */
.health-focus-bar {
    height: 3px;
    margin: 0 0 var(--space-4, 1rem);
    border-radius: 2px;
    background: color-mix(in srgb, var(--text-primary) 10%, transparent);
    overflow: hidden;
}

.health-focus-bar span {
    display: block;
    height: 100%;
    background: var(--accent-primary);
    transition: width 0.2s ease;
}

/* The title opens the bookmark; it reads as a title, not a link button. */
.health-focus-title-link {
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.health-focus-title-link:hover,
.health-focus-title-link:focus-visible {
    text-decoration: underline;
    text-underline-offset: 3px;
}

.health-focus-open {
    flex: 0 0 auto;
    white-space: nowrap;
}

.health-focus-card kbd {
    margin-left: var(--space-1-5, 0.4rem);
    padding: var(--space-hairline, 0.05rem) var(--space-1, 0.3rem);
    border: 1px solid var(--border-secondary);
    border-radius: var(--radius-3, 0.25rem);
    background: color-mix(in srgb, var(--text-primary) 6%, transparent);
    font-family: var(--font-mono, inherit);
    font-size: var(--text-2xs, 0.68rem);
    color: var(--text-secondary);
}

/* Why it is here: the reason, set apart with the theme's accent. */
.health-focus-why {
    margin: 0 0 var(--space-4, 1rem);
    padding: var(--space-1, 0.2rem) 0 var(--space-1, 0.2rem) var(--space-3, 0.75rem);
    border-left: 3px solid var(--accent-primary);
}

.health-focus-why-title {
    margin: 0 0 var(--space-1, 0.25rem);
    font-size: var(--text-base, 0.9rem);
    font-weight: 600;
    color: var(--text-primary);
}

.health-focus-why .health-focus-reasons {
    margin: 0;
    padding-left: 0;
    list-style: none;
    font-size: var(--text-sm, 0.82rem);
}

/* The one suggested next step: full width, the accent's outline. Scoped to
   the card so .config-btn's own colours do not win. */
.health-focus-card .health-focus-primary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    padding: var(--space-2, 0.6rem) var(--space-3, 0.8rem);
    border-color: var(--accent-primary);
    color: var(--accent-primary);
    font-weight: 600;
}

.health-focus-card .health-focus-primary:hover {
    background: color-mix(in srgb, var(--accent-primary) 10%, transparent);
}

.health-focus-card .health-focus-primary kbd {
    color: var(--accent-primary);
    border-color: color-mix(in srgb, var(--accent-primary) 50%, transparent);
}

/* The alternatives, and the "not now" row: text with its key, no boxes. */
.health-focus-link {
    display: inline-flex;
    align-items: center;
    padding: var(--space-1, 0.25rem) 0;
    border: 0;
    background: none;
    color: var(--text-primary);
    font: inherit;
    font-size: var(--text-sm, 0.85rem);
    cursor: pointer;
}

.health-focus-link:hover,
.health-focus-link:focus-visible {
    color: var(--accent-primary);
}

.health-focus-link.is-danger {
    color: var(--accent-error, var(--text-primary));
}

.health-focus-alts {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1, 0.25rem) var(--space-5, 1.25rem);
    margin: var(--space-2, 0.6rem) var(--space-1, 0.2rem) 0;
}

.health-focus-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2, 0.5rem);
    margin: var(--space-4, 1rem) 0 var(--space-2, 0.5rem);
    padding-top: var(--space-3, 0.7rem);
    border-top: 1px solid var(--border-primary);
}

.health-focus-foot-label {
    font-size: var(--text-sm, 0.8rem);
    color: var(--text-secondary);
}

.health-focus-foot-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1, 0.25rem) var(--space-4, 1rem);
}

.health-focus-foot .health-focus-link {
    color: var(--text-secondary);
}

.health-focus-foot .health-focus-link:hover {
    color: var(--text-primary);
}

/* The chooser: which pile, with what for. */
.health-focus-chooser-lead {
    margin: 0 0 var(--space-3, 0.8rem);
    font-size: var(--text-sm, 0.85rem);
    line-height: 1.5;
    color: var(--text-secondary);
}

.health-focus-piles {
    display: flex;
    flex-direction: column;
    gap: var(--space-1-5, 0.4rem);
}

.health-focus-pile {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 0.1rem var(--space-3, 0.75rem);
    align-items: center;
    width: 100%;
    padding: var(--space-2, 0.55rem) var(--space-3, 0.75rem);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-5, 0.5rem);
    background: transparent;
    color: var(--text-primary);
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.health-focus-pile:hover {
    border-color: var(--border-secondary);
}

.health-focus-pile.is-selected {
    border-color: var(--accent-primary);
    box-shadow: inset 3px 0 0 var(--accent-primary);
    background: color-mix(in srgb, var(--accent-primary) 7%, transparent);
}

.health-focus-pile-name {
    font-weight: 600;
    font-size: var(--text-base, 0.9rem);
}

.health-focus-pile-note {
    grid-column: 1;
    font-size: var(--text-sm, 0.8rem);
    color: var(--text-secondary);
}

.health-focus-pile-count {
    grid-column: 2;
    grid-row: 1 / 3;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.health-focus-chooser-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2, 0.5rem);
    margin-top: var(--space-4, 1rem);
}

.health-focus-chooser-foot .health-focus-card .health-focus-primary,
.health-focus-chooser-foot .health-focus-primary {
    width: auto;
    gap: var(--space-2, 0.5rem);
}

/* The end of a pile: four counts and what is next. */
.health-focus-stats {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--space-2, 0.5rem);
    margin: var(--space-3, 0.8rem) 0;
}

.health-focus-stat {
    padding: var(--space-2, 0.6rem);
    border-radius: var(--radius-5, 0.5rem);
    background: color-mix(in srgb, var(--text-primary) 5%, transparent);
    text-align: center;
}

.health-focus-stat b {
    display: block;
    font-size: var(--text-xl, 1.3rem);
    font-variant-numeric: tabular-nums;
}

.health-focus-stat span {
    font-size: var(--text-sm, 0.8rem);
    color: var(--text-secondary);
}

.health-focus-done-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2, 0.5rem);
    text-align: left;
}

.health-focus-done-foot .health-focus-done-rest,
.health-focus-done-foot .health-focus-actions {
    margin: 0;
}

@media (max-width: 30rem) {
    .health-focus-identity {
        flex-wrap: wrap;
    }

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

/* ==== css/config-setting-promo.css ==== */
/* One-time config setting highlight popovers (see config-setting-promo.js) */

.config-setting-promo-anchor-highlight {
    position: relative;
    z-index: 1;
    border-radius: var(--radius-6, 10px);
    outline: 2px solid color-mix(in srgb, var(--accent-primary, #4a90d9) 55%, transparent);
    outline-offset: 4px;
    animation: config-setting-promo-anchor-pulse 2.4s ease-in-out infinite;
}

@keyframes config-setting-promo-anchor-pulse {
    0%, 100% { outline-color: color-mix(in srgb, var(--accent-primary, #4a90d9) 40%, transparent); }
    50% { outline-color: color-mix(in srgb, var(--accent-primary, #4a90d9) 78%, transparent); }
}

.config-setting-promo {
    position: fixed;
    z-index: 1200;
    max-width: min(300px, calc(100vw - 24px));
    pointer-events: auto;
    opacity: 0;
    transform: translateY(8px) scale(0.98);
    transition: opacity 0.22s ease, transform 0.22s ease;
}

.config-setting-promo.is-visible {
    opacity: 1;
    transform: translateY(0) scale(1);
}

@keyframes config-setting-promo-float {
    0%, 100% { transform: translateY(0) scale(1); }
    50% { transform: translateY(-3px) scale(1); }
}

.config-setting-promo-inner {
    position: relative;
    background: var(--background-secondary);
    border: 1px solid var(--border-primary, var(--border-color, rgba(128, 128, 128, 0.35)));
    border-radius: var(--radius-7, 0.75rem);
    padding: var(--space-3, 0.85rem) var(--space-4, 0.95rem) var(--space-4, 0.9rem);
    box-shadow:
        0 10px 28px rgba(0, 0, 0, 0.22),
        0 0 0 1px color-mix(in srgb, var(--accent-primary, #4a90d9) 18%, transparent);
}

.config-setting-promo-badge {
    display: inline-block;
    margin-bottom: var(--space-1-5, 0.35rem);
    padding: var(--space-hairline, 0.12rem) var(--space-2, 0.45rem);
    border-radius: var(--radius-pill, 999px);
    font-size: var(--text-2xs, 0.68rem);
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--accent-primary, #4a90d9);
    background: color-mix(in srgb, var(--accent-primary, #4a90d9) 16%, transparent);
}

.config-setting-promo-title {
    margin: 0 0 var(--space-1-5, 0.35rem);
    font-size: calc(var(--font-size-text, 14px) * 1.05);
    font-weight: 600;
    color: var(--text-primary, inherit);
    line-height: 1.3;
}

.config-setting-promo-body {
    margin: 0 0 var(--space-3, 0.75rem);
    font-size: calc(var(--font-size-text, 14px) * 0.94);
    color: var(--text-secondary);
    line-height: 1.45;
}

.config-setting-promo-dismiss {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-1-5, 0.35rem) var(--space-3, 0.75rem);
    border: none;
    border-radius: var(--radius-5, 0.45rem);
    font: inherit;
    font-size: calc(var(--font-size-text, 14px) * 0.94);
    font-weight: 600;
    cursor: pointer;
    color: var(--background-primary, #fff);
    background: var(--accent-primary, #4a90d9);
    transition: filter 0.12s ease, transform 0.12s ease;
}

.config-setting-promo-dismiss:hover {
    filter: brightness(1.08);
}

.config-setting-promo-dismiss:focus-visible {
    outline: 2px solid var(--accent-primary, #4a90d9);
    outline-offset: 2px;
}

.config-setting-promo-arrow {
    position: absolute;
    width: 10px;
    height: 10px;
    background: var(--background-secondary);
    border: 1px solid var(--border-primary, var(--border-color, rgba(128, 128, 128, 0.35)));
    transform: rotate(45deg);
    box-shadow: -2px 2px 4px rgba(0, 0, 0, 0.08);
}

/* Below the control (default) */
.config-setting-promo.is-below .config-setting-promo-arrow {
    top: -5px;
    left: var(--promo-arrow-left, 50%);
    margin-left: -5px;
    border-right: none;
    border-bottom: none;
}

/* Above the control when there is no room below */
.config-setting-promo.is-above .config-setting-promo-arrow {
    bottom: -5px;
    left: var(--promo-arrow-left, 50%);
    margin-left: -5px;
    border-left: none;
    border-top: none;
}

/* Beside the control (optional placement) */
.config-setting-promo.is-right .config-setting-promo-arrow,
.config-setting-promo.is-left .config-setting-promo-arrow {
    top: 50%;
    margin-top: -5px;
    margin-left: 0;
}

.config-setting-promo.is-right .config-setting-promo-arrow {
    left: -5px;
    border-right: none;
    border-top: none;
}

.config-setting-promo.is-left .config-setting-promo-arrow {
    right: -5px;
    border-left: none;
    border-bottom: none;
}

@media (max-width: 720px) {
    .config-setting-promo {
        display: none !important;
    }

    .config-setting-promo-anchor-highlight {
        animation: none;
        outline-width: 1px;
    }
}

@media (prefers-reduced-motion: reduce) {
    .config-setting-promo,
    .config-setting-promo.is-visible,
    .config-setting-promo-anchor-highlight {
        animation: none !important;
        transition: none !important;
    }

    .config-setting-promo {
        opacity: 1;
        transform: none;
    }
}

body.no-animations .config-setting-promo,
body.no-animations .config-setting-promo.is-visible,
body.no-animations .config-setting-promo-anchor-highlight {
    animation: none !important;
    transition: none !important;
}

body.no-animations .config-setting-promo {
    opacity: 1;
    transform: none;
}

/* ==== css/colors.css ==== */
/* Colors editor — embedded in Config (#colors tab) */

.theme-colors-editor {
    display: flex;
    flex-direction: column;
    gap: var(--config-tab-gap, 0.85rem);
    width: 100%;
    max-width: 100%;
    box-sizing: border-box;
}

.theme-colors-editor--readonly .theme-colors-actions button:not(.colors-readonly-allowed),
.theme-colors-editor--readonly #add-custom-theme-btn,
.theme-colors-editor--readonly .custom-theme-move-btn,
.theme-colors-editor--readonly .custom-theme-list-item .btn-danger,
.theme-colors-editor--readonly .custom-theme-list-item input {
    opacity: 0.55;
    pointer-events: none;
}

.colors-mobile-readonly-banner {
    margin: 0;
    padding: var(--space-3, 0.65rem) var(--space-3, 0.85rem);
    border: 1px solid var(--border-primary);
    background: var(--background-secondary);
    color: var(--text-secondary);
    font-size: var(--font-size-text);
    line-height: 1.45;
}

.colors-contrast-hint {
    margin: 0;
    padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
    border-left: 3px solid var(--accent-warning);
    background: color-mix(in srgb, var(--accent-warning) 12%, transparent);
    color: var(--text-secondary);
    font-size: var(--font-size-text);
}

.color-text-input.color-input-invalid,
.color-text-input-full.color-input-invalid {
    border-color: var(--accent-error);
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-error) 35%, transparent);
}

.colors-palette-live-preview {
    margin-bottom: 1.25rem;
    position: static;
    top: auto;
}

.custom-theme-list-item {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2, 0.5rem);
}

.custom-theme-reorder-btns {
    display: flex;
    flex-direction: column;
    gap: var(--space-hairline, 0.15rem);
}

.custom-theme-move-btn {
    min-width: 2rem;
    padding: var(--space-hairline, 0.15rem) var(--space-1-5, 0.35rem);
    line-height: 1;
}

.theme-colors-intro .simple-tab-intro {
    display: block;
    line-height: 1.5;
}

.theme-colors-intro .help-text,
.theme-colors-intro p {
    margin: 0 0 var(--space-2, 0.5rem);
    color: var(--text-secondary);
    font-size: var(--font-size-text);
    line-height: 1.5;
}

.theme-colors-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3, 0.75rem) var(--space-4, 1rem);
    padding-bottom: var(--space-2, 0.5rem);
    border-bottom: 1px solid var(--border-primary);
}

.theme-colors-toolbar.config-tab-toolbar--in-surface {
    border: none;
    border-radius: 0;
    box-shadow: none;
    margin: 0;
    width: 100%;
    max-width: 100%;
    padding-bottom: var(--config-toolbar-padding-block, 0.5rem);
    background: var(--config-surface-toolbar, color-mix(in srgb, var(--background-primary) 35%, transparent));
}

/* Theme tab — fused surface (B10/C11) */
.theme-colors-tab-surface .theme-colors-body {
    display: flex;
    flex-direction: column;
    gap: 0;
    width: 100%;
    max-width: 100%;
    box-sizing: border-box;
}

.theme-colors-tab-surface .colors-tab-panel {
    padding: 0;
    border: none;
    border-radius: 0;
    box-shadow: none;
    background: transparent;
}

.theme-colors-tab-surface .colors-tab-panel .config-section {
    padding: var(--space-4, 0.9rem) var(--space-4, 1rem);
    border: none;
}

.theme-colors-tab-surface .colors-panel-hint,
.theme-colors-tab-surface .colors-tab-panel > .config-section > .help-text {
    margin: 0 0 var(--space-3, 0.75rem);
}

.theme-colors-tab-surface .page-selector-wrapper {
    margin: 0 0 var(--space-3, 0.75rem);
    padding: 0;
    border: none;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
}

.theme-colors-tab-surface .colors-grid {
    display: flex;
    flex-direction: column;
    gap: 0;
    margin-top: var(--space-3, 0.75rem);
    border: 1px solid var(--border-primary);
    background: var(--config-surface-list, transparent);
}

.theme-colors-tab-surface .color-group {
    padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
    border-bottom: 1px solid var(--border-primary);
}

.theme-colors-tab-surface .color-group:last-child {
    border-bottom: none;
}

.theme-colors-tab-surface .color-group h3 {
    margin: 0 0 var(--space-2, 0.5rem);
}

.theme-colors-tab-surface .color-item {
    margin-bottom: 0;
    padding: var(--space-2, 0.45rem) 0;
    min-height: 2.25rem;
    box-sizing: border-box;
    border-bottom: 1px solid var(--border-primary);
    background: transparent;
    transition: background-color 0.12s ease;
}

.theme-colors-tab-surface .color-group .color-item:last-child {
    border-bottom: none;
}

.theme-colors-tab-surface .color-item:hover {
    background: var(--config-row-hover, color-mix(in srgb, var(--accent-primary) 8%, transparent));
}

.theme-colors-tab-surface #custom-themes-list.categories-list {
    border: 1px solid var(--border-primary);
    border-radius: 0;
    background: transparent;
    box-shadow: none;
}

.theme-colors-tab-surface .custom-themes-main-grid {
    gap: 0;
    margin-top: var(--space-4, 1rem);
    border-top: 1px solid var(--border-primary);
}

.theme-colors-tab-surface .custom-themes-list-section {
    padding: var(--space-4, 0.9rem) var(--space-4, 1rem) var(--space-4, 1rem) 0;
    border-right: 1px solid var(--border-primary);
}

.theme-colors-tab-surface .custom-theme-colors-section {
    border-left: none;
    padding: var(--space-4, 0.9rem) 0 var(--space-4, 1rem) var(--space-4, 1rem);
    margin-top: 0;
}

.theme-colors-tab-surface .theme-preview-card {
    border-radius: 0;
    box-shadow: none;
    border: 1px solid var(--border-primary);
    margin-bottom: var(--space-4, 1rem);
}

.theme-colors-tab-surface .theme-preview-label {
    background: var(--config-surface-toolbar, color-mix(in srgb, var(--background-primary) 25%, transparent));
}

@media (max-width: 1024px) {
    .theme-colors-tab-surface .custom-themes-main-grid {
        border-top: 1px solid var(--border-primary);
    }

    .theme-colors-tab-surface .custom-themes-list-section {
        padding: var(--space-4, 0.9rem) var(--space-4, 1rem) 0;
        border-right: none;
        border-bottom: 1px solid var(--border-primary);
    }

    .theme-colors-tab-surface .custom-theme-colors-section {
        padding: var(--space-4, 0.9rem) var(--space-4, 1rem) var(--space-4, 1rem);
    }
}

.theme-colors-toolbar {
    justify-content: space-between;
}

.theme-colors-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-2, 0.5rem);
    margin-left: auto;
}

.theme-colors-info-btn {
    flex-shrink: 0;
    align-self: center;
}

.colors-subtabs-header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2, 0.5rem);
}

.theme-colors-section-heading {
    display: flex;
    align-items: center;
    gap: var(--space-1-5, 0.35rem);
    margin-bottom: var(--space-4, 1rem);
    font-size: var(--font-size-text);
    font-weight: var(--font-weight-bold);
    letter-spacing: 0.05em;
    color: var(--text-tertiary);
    text-transform: lowercase;
}

.theme-colors-section-heading::before {
    content: '// ';
}

.colors-panel-hint {
    margin: 0 0 var(--space-4, 1rem);
}

/* colors unsaved badge: config-unsaved-state.css (.unsaved-badge) */

.colors-subtabs {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 0;
    padding: 0;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-3, 4px);
    overflow: visible;
    background: transparent;
}

.colors-tab-button {
    appearance: none;
    border: none;
    border-radius: 0;
    background: transparent;
    color: var(--text-secondary);
    font: inherit;
    font-size: var(--text-sm, 0.85rem);
    font-weight: inherit;
    padding: var(--space-1-5, 0.35rem) var(--space-3, 0.75rem);
    cursor: pointer;
    text-transform: lowercase;
    transition: background 0.15s ease, color 0.15s ease;
    position: relative;
}

.colors-tab-button + .colors-tab-button {
    border-left: 1px solid var(--border-primary);
}

.colors-tab-button:hover {
    color: var(--text-primary);
    background: color-mix(in srgb, var(--accent-primary) 12%, transparent);
}

.colors-tab-button.active {
    color: var(--text-primary);
    background: color-mix(in srgb, var(--accent-primary) 22%, transparent);
    font-weight: var(--font-weight-bold, 600);
}

.colors-tab-button:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
}

.colors-tab-panel[hidden] {
    display: none !important;
}

.colors-grid {
    display: flex;
    flex-direction: column;
    gap: var(--space-7, 3rem);
    margin-top: var(--space-4, 1rem);
}

.color-group {
    border: none;
    padding: 0;
}

.color-group h3 {
    margin-top: 0;
    margin-bottom: var(--space-4, 1rem);
    font-size: var(--font-size-text);
    font-weight: var(--font-weight-bold);
    letter-spacing: 0.05em;
    color: var(--text-tertiary);
    text-transform: lowercase;
    border: none;
    padding: 0;
}

.color-group h3::before {
    content: '// ';
}

.color-item {
    margin-bottom: var(--space-4, 1rem);
    display: flex;
    align-items: center;
    gap: var(--space-4, 1rem);
}

.color-item:last-child {
    margin-bottom: 0;
}

.color-item label {
    font-size: var(--font-size-text);
    color: var(--text-secondary);
    font-weight: var(--font-weight-semibold);
    min-width: var(--config-label-width, 10rem);
    margin: 0;
}

.color-input-wrapper {
    display: flex;
    gap: var(--space-2, 0.5rem);
    align-items: center;
    flex: 1;
}

.color-input-wrapper input[type="color"] {
    width: 40px;
    height: 28px;
    border: none;
    border-bottom: 1px solid var(--border-primary);
    border-radius: 0;
    cursor: pointer;
    background: transparent;
    padding: 0;
    transition: border-color 0.2s ease;
}

.color-input-wrapper input[type="color"]:hover {
    border-bottom-color: var(--text-secondary);
}

.color-input-wrapper input[type="color"]:focus {
    outline: none;
    border-bottom-color: var(--text-primary);
}

.color-text-input {
    flex: 1;
    min-width: 120px;
    padding: var(--space-1-5, 0.375rem) var(--space-2, 0.5rem);
    border: 1px solid var(--border-primary);
    background: var(--background-primary);
    color: var(--text-primary);
    font-family: inherit;
    font-size: var(--font-size-text);
    transition: border-color 0.3s ease;
}

.color-text-input-full {
    width: 100%;
    max-width: 300px;
    padding: var(--space-1-5, 0.375rem) var(--space-2, 0.5rem);
    border: 1px solid var(--border-primary);
    background: var(--background-primary);
    color: var(--text-primary);
    font-family: inherit;
    font-size: var(--font-size-text);
    transition: border-color 0.3s ease;
}

.color-text-input:focus,
.color-text-input-full:focus {
    outline: none;
    border-color: var(--text-secondary);
}

.color-text-input:hover,
.color-text-input-full:hover {
    border-color: var(--text-secondary);
}

/* Responsive adjustments for colors page */
@media (max-width: 768px) {
    .theme-colors-toolbar {
        flex-direction: column;
        align-items: stretch;
    }

    .theme-colors-actions,
    .colors-subtabs {
        width: 100%;
    }

    .colors-subtabs {
        overflow-x: auto;
        flex-wrap: nowrap;
        padding-bottom: var(--space-1, 0.25rem);
    }

    .color-item {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--space-2, 0.5rem);
    }

    .color-item label {
        min-width: auto;
    }

    .color-input-wrapper {
        width: 100%;
    }

    .color-text-input {
        max-width: 100%;
    }

    .color-input-wrapper input[type="color"] {
        width: 36px;
        height: 24px;
    }
}

/* Custom Themes Section */
.custom-themes-main-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-7, 3rem);
    margin-top: var(--space-6, 2rem);
}

.custom-themes-list-section h3 {
    margin-bottom: var(--space-4, 1rem);
    font-size: var(--font-size-text);
    font-weight: var(--font-weight-bold);
    letter-spacing: 0.05em;
    color: var(--text-tertiary);
    text-transform: lowercase;
}

.custom-themes-list-section h3::before {
    content: '// ';
}

#custom-themes-list {
    margin-bottom: var(--space-4, 1rem);
}

.custom-themes-list-section .category-item {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: var(--space-3, 0.75rem);
    align-items: center;
}

.custom-themes-list-section .category-item input {
    width: 100%;
}

.custom-themes-list-section .btn-danger {
    min-width: 88px;
    padding: var(--space-1-5, 0.375rem) var(--space-2, 0.625rem);
}

#add-custom-theme-btn {
    margin-top: var(--space-1, 0.25rem);
}

.custom-theme-colors-section {
    border-left: 1px solid var(--border-primary);
    padding-left: var(--space-6, 2rem);
}

.custom-theme-colors-section .colors-grid {
    margin-top: 0;
}

/* Theme preview card */
.theme-preview-card {
    position: sticky;
    top: 6rem;
    margin-bottom: var(--space-6, 2rem);
    border: 1px solid var(--border-primary);
    background: var(--background-primary);
    font-size: var(--font-size-controls);
    user-select: none;
    pointer-events: none;
}

.theme-preview-label {
    padding: var(--space-1-5, 0.35rem) var(--space-3, 0.75rem);
    font-size: 0.72em;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-secondary);
    border-bottom: 1px solid var(--border-primary);
    background: var(--background-secondary);
}

.theme-preview-body {
    padding: var(--space-3, 0.75rem);
    display: flex;
    flex-direction: column;
    gap: var(--space-3, 0.75rem);
}

.theme-preview-category {
    border: 1px solid var(--border-primary);
    background: var(--background-secondary);
    padding: var(--space-2, 0.5rem) var(--space-3, 0.65rem);
}

.theme-preview-category-title {
    display: block;
    font-size: 0.78em;
    font-weight: var(--font-weight-bold);
    color: var(--text-tertiary);
    margin-bottom: var(--space-1-5, 0.4rem);
}

.theme-preview-bookmark {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: var(--space-1, 0.25rem) 0;
    border-bottom: 1px solid var(--border-primary);
}

.theme-preview-bookmark:last-child {
    border-bottom: none;
}

.theme-preview-bm-name {
    color: var(--text-primary);
    font-size: 0.9em;
}

.theme-preview-bm-shortcut {
    color: var(--text-tertiary);
    font-size: 0.75em;
    opacity: 0.65;
}

.theme-preview-controls {
    display: flex;
    gap: var(--space-1-5, 0.4rem);
    flex-wrap: wrap;
    align-items: center;
}

.theme-preview-input {
    flex: 1;
    min-width: 0;
    padding: var(--space-1, 0.25rem) var(--space-2, 0.5rem);
    border: 1px solid var(--border-primary);
    background: var(--background-primary);
    color: var(--text-secondary);
    font-family: inherit;
    font-size: 0.85em;
}

.theme-preview-btn {
    padding: var(--space-1, 0.25rem) var(--space-2, 0.6rem);
    border: 1px solid var(--border-primary);
    background: var(--background-secondary);
    color: var(--text-primary);
    font-family: inherit;
    font-size: 0.85em;
    cursor: default;
}

.theme-preview-btn-success {
    border-color: var(--accent-success);
    color: var(--accent-success);
}

.theme-preview-btn-danger {
    border-color: var(--accent-error);
    color: var(--accent-error);
}

.theme-preview-badges {
    display: flex;
    gap: var(--space-1-5, 0.4rem);
    flex-wrap: wrap;
}

.theme-preview-badge {
    padding: var(--space-hairline, 0.15rem) var(--space-2, 0.5rem);
    font-size: 0.78em;
    border: 1px solid currentColor;
}

.theme-preview-badge-success { color: var(--accent-success); }
.theme-preview-badge-warning { color: var(--accent-warning); }
.theme-preview-badge-error   { color: var(--accent-error); }

@media (max-width: 1024px) {
    .custom-themes-main-grid {
        grid-template-columns: 1fr;
        gap: var(--space-6, 2rem);
    }

    .custom-theme-colors-section {
        border-left: none;
        border-top: 1px solid var(--border-primary);
        padding-left: 0;
        padding-top: var(--space-5, 1.5rem);
    }

    .theme-preview-card {
        position: static;
    }
}

/* ── The state bloom, applied ───────────────────────────────────────────────
 *
 * The token lives in theme-depth.css with the rest of the depth ladder; the
 * application lives here, because this file loads after the view stylesheets
 * and box-shadow does not stack across rules. From theme-depth.css the
 * bookmark row's own `box-shadow: none` won and the cursor never glowed.
 *
 * One rule rather than a hundred: these are the app's live-state surfaces --
 * the control with focus, the row the keyboard cursor is on, the filter in
 * force. Everything else keeps the shadow it already had, which is why this
 * is a list and not a blanket `*:focus-visible`.
 *
 * The outline is untouched throughout. This is decoration on top of it, not a
 * replacement for it.
 */
body:not([data-depth="flat"]) :is(
    .search-button,
    .page-nav-btn,
    .lvs-filter,
    .lvs-section,
    .config-nav-item,
    .tag-cloud-toggle-btn,
    .whats-new-btn
):focus-visible {
    box-shadow: var(--bloom);
}

body:not([data-depth="flat"]) :is(
    .bookmark-link.keyboard-selected,
    .feed-row.keyboard-selected,
    .lvs-filter.is-active
) {
    box-shadow: var(--bloom);
}

/* ==== css/enhanced-features.css ==== */
/* Enhanced Features CSS */

/* The bare `kbd` rule below is the one that actually applies: this file
   loads after dashboard-enhancements.css, which declares kbd too, so this
   copy wins on order. It is kept here rather than folded into that file to
   avoid changing how every keyboard hint renders. */
kbd {
    background: var(--background-primary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-3, 4px);
    padding: 4px 8px;
    font-family: monospace;
    font-size: 12px;
    color: var(--text-primary);
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    white-space: nowrap;
}

/* ===== Empty States ===== */
.empty-state {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    padding: var(--space-7, 3rem) 0;
    text-align: left;
    color: var(--text-secondary);
    max-width: 40ch;
}

.empty-state-label {
    font-size: var(--font-size-category);
    font-weight: var(--font-weight-bold);
    color: var(--text-tertiary);
    margin-bottom: var(--space-3, 0.75rem);
    letter-spacing: 0.04em;
}

.empty-state-text {
    font-size: var(--font-size-text);
    font-weight: var(--font-weight-semibold);
    color: var(--text-primary);
    margin-bottom: var(--space-2, 0.5rem);
}

.empty-state-action {
    margin-top: var(--space-5, 1.5rem);
    display: flex;
    gap: var(--space-3, 0.75rem);
    flex-wrap: wrap;
}

.empty-state-actions {
    margin-top: 1.25rem;
    display: flex;
    flex-direction: column;
    gap: var(--space-1-5, 0.4rem);
}

.empty-state-hint {
    margin: var(--space-3, 0.85rem) 0 0;
    font-size: calc(var(--font-size-controls) * 0.92);
    color: var(--text-tertiary);
    line-height: 1.45;
    max-width: 22rem;
}

.empty-state-action-btn--primary {
    border-color: color-mix(in srgb, var(--accent-primary) 65%, var(--border-primary));
    background: color-mix(in srgb, var(--accent-primary) 14%, var(--background-primary));
    font-weight: var(--font-weight-semibold);
}

.empty-state-action-btn--primary:hover {
    background: color-mix(in srgb, var(--accent-primary) 22%, var(--background-primary));
}

.empty-state-action-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.5em;
    background: none;
    border: none;
    padding: var(--space-1, 0.2rem) 0;
    font-size: var(--font-size-controls);
    color: var(--text-secondary);
    cursor: pointer;
    text-align: left;
    transition: color 0.12s ease;
}

.empty-state-action-btn:hover {
    color: var(--text-primary);
}

.empty-state-action-btn kbd {
    font-family: var(--font-family-main);
    font-size: 0.9em;
    color: var(--text-primary);
    background: var(--background-secondary);
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-2, 3px);
    padding: 0 0.3em;
    transition: background 0.12s ease;
}

.empty-state-action-btn:hover kbd {
    background: color-mix(in srgb, var(--accent-primary) 15%, var(--background-secondary));
    border-color: var(--accent-primary);
}

/* Secondary text links under the fresh empty state (add modal, config, import).
   Styled locally because the global .btn styles are config-only and don't load
   on the dashboard — bare <a> would otherwise render as raw browser links. */
.empty-state-links {
    margin-top: var(--space-5, 1.5rem);
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-2, 0.55rem);
}

.empty-state-link {
    display: inline-flex;
    align-items: center;
    background: none;
    border: none;
    padding: 0;
    font-family: inherit;
    font-size: calc(var(--font-size-controls) * 0.92);
    color: var(--text-secondary);
    text-decoration: none;
    border-bottom: 1px dotted color-mix(in srgb, var(--text-secondary) 55%, transparent);
    cursor: pointer;
    transition: color 0.12s ease, border-color 0.12s ease;
}

.empty-state-link:hover {
    color: var(--text-primary);
    border-bottom-color: var(--accent-primary);
}

/* ===== Lazy Load Placeholders ===== */
.icon-placeholder {
    background: linear-gradient(90deg, var(--background-modal) 25%, var(--background-primary) 50%, var(--background-modal) 75%);
    background-size: 200% 100%;
    animation: loading 1.5s infinite;
    border-radius: var(--radius-3, 4px);
    width: 1.15em;
    height: 1.15em;
    display: inline-block;
}

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

/* ==== css/mobile-experience.css ==== */
/* ── Mobile / compact layout overrides ── */

.mobile-experience-banner {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
    margin: 0 0 var(--space-3, 0.75rem);
    padding: var(--space-3, 0.65rem) var(--space-3, 0.85rem);
    border: 1px solid color-mix(in srgb, var(--accent-primary) 45%, var(--border-primary));
    background: color-mix(in srgb, var(--accent-primary) 8%, var(--background-primary));
    border-radius: var(--radius-4, 0.35rem);
    font-size: var(--font-size-controls);
    line-height: 1.45;
    color: var(--text-secondary);
}

.mobile-experience-banner[hidden] {
    display: none !important;
}

.mobile-experience-banner-text {
    margin: 0;
    flex: 1 1 14rem;
}

.mobile-experience-banner-text a {
    color: var(--accent-primary);
    font-weight: var(--font-weight-semibold);
}

.mobile-experience-banner-actions {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    gap: var(--space-1-5, 0.35rem);
}

.mobile-experience-banner-dismiss {
    border: 1px solid var(--border-primary);
    background: var(--background-secondary);
    color: var(--text-secondary);
    width: 2rem;
    height: 2rem;
    border-radius: var(--radius-3, 0.25rem);
    cursor: pointer;
    font-size: var(--text-xl, 1.25rem);
    line-height: 1;
    padding: 0;
}

.mobile-experience-banner-dismiss:hover {
    color: var(--text-primary);
}

/* ── Dashboard: footer — search + add bookmark on mobile ── */
body[data-mobile-layout="true"] #finders-button,
body[data-mobile-layout="true"] #commands-button,
body[data-mobile-layout="true"] #recent-bookmarks-button,
body[data-mobile-layout="true"] #help-button,
body[data-mobile-layout="true"] #whats-new-btn,
body[data-mobile-layout="true"] .btn-group-secondary {
    display: none !important;
}

/* The What's new star also stays out of a phone-width window without a touch
   screen (a narrowed browser, a side panel): it floats over the list there
   too, and Config → About carries the same news. */
@media (max-width: 767px) {
    #whats-new-btn {
        display: none !important;
    }
}

body[data-mobile-layout="true"] #quick-add-toolbar-btn {
    display: flex !important;
    order: -1;
    flex: 1 1 auto;
    min-height: 44px;
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
}

body[data-mobile-layout="true"] .button-container {
    justify-content: center;
    width: calc(100% - 2rem);
    flex-wrap: wrap;
    row-gap: var(--space-1-5, 0.35rem);
}

body[data-mobile-layout="true"] .btn-group-primary {
    justify-content: center;
    width: 100%;
    gap: var(--space-2, 0.5rem);
}

body[data-mobile-layout="true"] #search-button {
    flex: 1 1 auto;
    max-width: none;
    min-height: 44px;
}

/* Dashboard: single column on a phone held upright -- ignore columnsPerRow
   and the packed layout. Only there: a narrow desktop window keeps the
   columns that fit, which the grid's own column count already says. */
body[data-dashboard-stack-categories="true"] .dashboard-grid {
    display: grid !important;
    grid-template-columns: 1fr !important;
}

body[data-dashboard-stack-categories="true"] .dashboard-grid.packed-columns {
    display: flex !important;
    flex-direction: column !important;
    flex-wrap: nowrap !important;
}

body[data-dashboard-stack-categories="true"] .dashboard-grid.packed-columns .dashboard-column {
    flex: 1 1 100% !important;
    max-width: 100% !important;
    width: 100% !important;
}

/* Search mode tabs visible on mobile (override search.css ≤479px hide) */
body[data-mobile-layout="true"] .search-mode-tabs {
    display: flex !important;
}

body[data-mobile-layout="true"] .search-mode-tab {
    min-height: 44px;
    flex: 1 1 0;
}

/* Header simplification */
body[data-mobile-layout="true"] .library-link,
body[data-mobile-layout="true"] #page-navigation,
body[data-mobile-layout="true"] #page-nav-inbox-host,
body[data-mobile-layout="true"] #dashboard-tag-cloud-wrap,
body[data-mobile-layout="true"] #status-loading-indicator {
    display: none !important;
}

/* Header: Pages + config only — touch-friendly targets */
body[data-mobile-layout="true"] .header-actions {
    gap: var(--space-2, 0.5rem);
    flex-shrink: 0;
}

body[data-mobile-layout="true"] #page-overview-header-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    min-width: 44px;
    padding: var(--space-2, 0.55rem);
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
}

body[data-mobile-layout="true"] #page-overview-header-btn:active {
    background: color-mix(in srgb, var(--accent-primary) 12%, var(--background-secondary));
}

body[data-mobile-layout="true"] .config-link a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    min-width: 44px;
    /* Symmetric now the label is an icon; the wider side padding was there to
       sit around the word "config". */
    padding: var(--space-2, 0.55rem);
    border: 1px solid color-mix(in srgb, var(--text-secondary) 35%, transparent);
    border-radius: var(--header-chip-radius, var(--layout-radius-pill, 999px));
    background: color-mix(in srgb, var(--background-secondary) 55%, transparent);
    text-transform: capitalize;
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
}

/* The icon variant's negative pull is for sitting beside the health icon, which
   mobile hides — without this the chip would overlap the pages button. */
body[data-mobile-layout="true"] .config-link--icon {
    margin-left: 0;
}

body[data-mobile-layout="true"] .config-link a:active,
body[data-mobile-layout="true"] #page-overview-header-btn:active {
    background: color-mix(in srgb, var(--accent-primary) 10%, transparent);
    color: var(--text-primary);
    border-color: var(--text-primary);
}

/* Link preview cards — no hover on touch */
body[data-mobile-hide-previews="true"] .link-preview-card,
body[data-mobile-hide-previews="true"] .bookmark-preview-popup {
    display: none !important;
}

/* ── Config: tabs & general panels (phone only — tablets keep full config) ── */
body[data-phone-layout="true"] .tab-button:not([data-tab="general"]):not([data-tab="help"]):not([data-tab="colors"]),
body[data-phone-layout="true"] .config-keyboard-chip {
    display: none !important;
}

body[data-phone-layout="true"] [data-general-panel][data-mobile-panel-hidden="true"],
body[data-phone-layout="true"] #general-layer-toolbar {
    display: none !important;
}

/* Config General — touch-friendly theme & language selects */
body[data-mobile-layout="true"] .form-group--theme-select .custom-select-trigger,
body[data-mobile-layout="true"] .form-group:has(#language-select) .custom-select-trigger {
    min-height: 44px;
    padding: var(--space-3, 0.65rem) var(--space-3, 0.85rem);
    touch-action: manipulation;
}

body[data-mobile-layout="true"] .custom-select-option {
    min-height: 44px;
    padding: var(--space-3, 0.65rem) var(--space-3, 0.85rem);
    display: flex;
    align-items: center;
}

body[data-mobile-layout="true"] .form-group--theme-select label,
body[data-mobile-layout="true"] .form-group:has(#language-select) label {
    font-weight: var(--font-weight-semibold);
}

body[data-mobile-layout="true"] .general-keyboard-link-row {
    display: none !important;
}

/* Search-flow hint — desktop discoverability only */
body[data-mobile-layout="true"] .search-flow-hint {
    display: none !important;
}

/* Toasts above the mobile search footer */
body[data-mobile-layout="true"] .app-notification {
    bottom: max(4.75rem, calc(env(safe-area-inset-bottom) + 3.75rem));
    max-width: min(420px, calc(100vw - 1.25rem));
}

/* ── Modals: mobile sheet ──
   Touch-only, mirroring isMobileLayout() in mobile-experience.js. Width alone
   would turn every narrow desktop window into a full-screen sheet. */
@media (hover: none) and (pointer: coarse) and (max-width: 991px) {
    .modal-overlay.show .modal,
    .modal-overlay.show .modal.modal--mobile-sheet {
        width: 100% !important;
        max-width: none !important;
        margin: 0;
        border-radius: 0;
        min-height: min(100dvh, 100vh);
        max-height: min(100dvh, 100vh);
    }

    .modal-overlay.show .modal .modal-body {
        max-height: calc(100dvh - 9rem);
        overflow-y: auto;
        -webkit-overflow-scrolling: touch;
    }

    .modal-overlay.show .modal .modal-actions {
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-2, 0.5rem);
    }

    .modal-overlay.show .modal .modal-button {
        min-height: 44px;
        width: 100%;
        justify-content: center;
    }

    .shortcut-search .search-container {
        max-height: calc(100dvh - 2rem);
    }
}

/* ==== css/dashboard-inline-edit-surface.css ==== */
/* Inline edit opaque surfaces — loaded last; wins over layout tokens. */

body.bookmark-inline-edit-active {
    --inline-edit-panel-bg: var(--background-primary);
    --inline-edit-field-bg: var(--background-secondary);
}

@supports (background: rgb(from red r g b)) {
    body.bookmark-inline-edit-active {
        --inline-edit-panel-bg: rgb(from var(--background-primary) r g b / 1);
        --inline-edit-field-bg: rgb(from var(--background-secondary) r g b / 1);
    }
}

body.bookmark-inline-edit-active .bookmark-link.bookmark-inline-editing {
    background: var(--inline-edit-panel-bg);
    border-radius: var(--radius-5, 8px);
    padding: var(--space-1-5, 0.35rem);
    box-shadow: 0 16px 48px color-mix(in srgb, var(--text-primary) 22%, transparent);
}

body.bookmark-inline-edit-active .layout-launcher .bookmark-link.bookmark-inline-editing {
    padding: 0;
    background: transparent;
    box-shadow: none;
}

body.bookmark-inline-edit-active .bookmark-inline-form {
    background: var(--inline-edit-panel-bg);
    background-image: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}

body.bookmark-inline-edit-active .bookmark-inline-form .bookmark-inline-input,
body.bookmark-inline-edit-active .bookmark-inline-form .bookmark-inline-select,
body.bookmark-inline-edit-active .bookmark-inline-form .bookmark-inline-textarea,
body.bookmark-inline-edit-active .bookmark-inline-form .bookmark-inline-action-btn,
body.bookmark-inline-edit-active .bookmark-inline-form .bookmark-inline-icon-preview {
    background: var(--inline-edit-field-bg);
}

/* ==== css/dashboard-media-tile.css ==== */
/*
 * Media tiles (Jellyfin, Sonarr, Radarr). Pictures sit in fixed ratios so nothing jumps while they
 * load; what only fits when the tile is wide answers to the panel's container
 * query through .dashboard-widget-wide-only, like every other tile.
 */
.media-panel { display: flex; flex-direction: column; gap: 0.5rem; }

.media-art {
    position: relative;
    display: block;
    flex: none;
    overflow: hidden;
    border-radius: var(--radius-sm, 4px);
    background: color-mix(in srgb, currentColor 10%, transparent);
}
.media-art--poster { width: 2.25rem; aspect-ratio: 2 / 3; }
.media-art--backdrop { width: 100%; aspect-ratio: 16 / 9; }
.media-art img { display: block; width: 100%; height: 100%; object-fit: cover; }
.media-art.is-missing { display: grid; place-items: center; }
.media-art-initials { font-size: var(--font-size-small); color: var(--text-tertiary); }

.media-badge {
    position: absolute;
    top: 0.2rem;
    right: 0.2rem;
    padding: 0 0.25rem;
    border-radius: var(--radius-pill, 999px);
    background: var(--accent-primary);
    color: var(--bg-primary, #000);
    font-size: 0.65rem;
    line-height: 1.3;
}
.media-badge--played { left: 0.2rem; right: auto; background: var(--accent-success); }

/*
 * minmax(0, 1fr), not 1fr: a track's minimum is its content's, and a title is
 * one unbroken line. A long one made the first column as wide as the title and
 * shoved the second out of the tile.
 */
.media-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.25rem 1rem; }
.media-row {
    min-width: 0;
    display: flex;
    gap: 0.6rem;
    align-items: flex-start;
    width: 100%;
    padding: 0.25rem 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.media-row-text { display: flex; flex-direction: column; min-width: 0; }
.media-title, .media-caption { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.media-meta { color: var(--text-tertiary); font-size: var(--font-size-small); }
.media-overview {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    color: var(--text-secondary, inherit);
    font-size: var(--font-size-small);
}
.media-stale { margin: 0; color: var(--accent-warning); font-size: var(--font-size-small); }
/* A download client: no pictures, so the text and its bar take the row. */
.media-row--bare .media-row-text { flex: 1 1 auto; }
.media-totals {
    margin: 0 0 0.35rem;
    color: var(--text-secondary, inherit);
    font-size: var(--font-size-small);
    font-variant-numeric: tabular-nums;
}

.media-tabs { display: flex; gap: 0.75rem; }
.media-tab {
    padding: 0 0 0.1rem;
    border: 0;
    border-bottom: 1px solid transparent;
    background: none;
    color: var(--text-tertiary);
    font: inherit;
    cursor: pointer;
}
.media-tab[aria-selected="true"] { color: inherit; border-bottom-color: var(--accent-primary); }

.media-strip { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.4rem; }
.media-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.4rem; }
.media-poster {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    min-width: 0;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.media-poster .media-art--poster { width: 100%; }
.media-caption { font-size: var(--font-size-small); }

/* Narrow grid: the title appears on focus, over the poster. */
.media-grid .media-poster { position: relative; }
.media-grid .media-poster:focus-visible::after,
.media-grid .media-poster:hover::after {
    content: attr(data-title);
    position: absolute;
    inset: auto 0 0 0;
    padding: 0.2rem 0.3rem;
    background: color-mix(in srgb, #000 65%, transparent);
    color: #fff;
    font-size: 0.7rem;
}

.media-hero-layout { display: grid; gap: 0.5rem; }
.media-hero {
    position: relative;
    display: block;
    width: 100%;
    padding: 0;
    border: 0;
    background: none;
    color: #fff;
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.media-hero .media-art { width: 100%; }
.media-hero-caption {
    position: absolute;
    inset: auto 0 0 0;
    display: flex;
    flex-direction: column;
    padding: 1.5rem 0.6rem 0.4rem;
    background: linear-gradient(transparent, color-mix(in srgb, #000 75%, transparent));
}
.media-hero-caption .media-meta,
.media-hero-caption .media-overview { color: color-mix(in srgb, #fff 80%, transparent); }

.media-detail {
    width: min(32rem, calc(100vw - 2rem));
    padding: 0;
    border: 1px solid var(--border-primary);
    border-radius: var(--radius-md, 8px);
    background: var(--bg-primary, Canvas);
    color: inherit;
}
.media-detail::backdrop { background: color-mix(in srgb, #000 55%, transparent); }
.media-detail > :not(.media-art) { margin: 0.5rem 1rem; }
.media-detail .media-art--poster { width: 8rem; margin: 1rem auto 0; }
.media-detail-title { font-size: 1.1rem; }
.media-detail-overview { line-height: 1.5; }
.media-detail-actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-bottom: 1rem !important; }

@container (min-width: 28rem) {
    .media-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .media-row .media-art--poster { width: 3rem; }
    .media-strip { grid-template-columns: repeat(8, minmax(0, 1fr)); }
    .media-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
    .media-grid .media-poster:focus-visible::after,
    .media-grid .media-poster:hover::after { content: none; }
    .media-hero-layout { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; }
    .media-list--side { grid-template-columns: minmax(0, 1fr); }
}

/* How far a download is. --p is the share, 0..1, set by the tile. */
.media-progress {
    display: block;
    height: 3px;
    margin-top: 0.25rem;
    border-radius: var(--radius-pill, 999px);
    background: linear-gradient(to right, var(--accent-primary) calc(var(--p, 0) * 100%),
        color-mix(in srgb, currentColor 15%, transparent) 0);
}

/* ==== css/reduced-motion.css ==== */
/* Global reduced-motion honouring.
 *
 * Animation suppression used to be opt-in per component: each effect paired a
 * `body.no-animations` rule (the in-app setting) with a matching
 * prefers-reduced-motion block. That left gaps — newer stylesheets such as
 * dashboard-tag-cloud.css and layout-side-rail.css never got the second half,
 * so an infinitely pulsing FAB kept running for someone who had asked their OS
 * to stop exactly that.
 *
 * The OS preference is an accessibility signal, not a styling preference, so it
 * wins outright rather than being merged with the in-app toggle. There is
 * deliberately no escape hatch: animationsEnabled defaults to true and is a
 * plain bool, so "explicitly on" is indistinguishable from "never touched" —
 * any opt-out would fire for every default user and undo the whole rule.
 *
 * Loaded last so this beats the `!important` declarations components already
 * use. Durations are near-zero rather than `animation: none`, because code that
 * waits on animationend (see dashboard-visual.js showHealthAlert) still needs
 * the event to fire; `none` would strand those listeners.
 */

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        animation-delay: 0s !important;
        transition-duration: 0.01ms !important;
        transition-delay: 0s !important;
        scroll-behavior: auto !important;
    }
}

/* ==== css/fonts.css ==== */
/* Self-hosted Source Code Pro (variable font) — replaces the Google Fonts CDN.
 * One woff2 per subset covers all weights (200–900). Keeps the app offline-first
 * and avoids a render-blocking third-party request. */

/* latin-ext (accented Latin, extra currency/symbol glyphs) */
@font-face {
    font-family: 'Source Code Pro';
    font-style: normal;
    font-weight: 200 900;
    font-display: swap;
    src: url('/static/fonts/source-code-pro-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* latin (ASCII + common punctuation/symbols) */
@font-face {
    font-family: 'Source Code Pro';
    font-style: normal;
    font-weight: 200 900;
    font-display: swap;
    src: url('/static/fonts/source-code-pro-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
