/* ===========================================================================
   chrome.css — the shared chrome's ONE stylesheet (packages/hub-chrome).

   Loaded on every page of the Hub and of every app, immediately after Pico and
   BEFORE the app's own stylesheet and the theme files (views/chrome/head.ejs
   fixes that order; it is the cascade). Built 2026-09-02 from three sources:

     1. THE SCP DESIGN TOKENS — the plain :root / [data-theme] token blocks that
        opened the Asset Manager's styles.css: the --pico-* overrides that turn
        Pico's blue into the SCP palette (teal #00b5d0, amber #feba12, salmon
        #f26c52, grey #58585a, from logo_and_colours/primary_colours.png and the
        extended tint/shade scales) and the app's own --* variables. Tokens only,
        no component rules: they are what makes a Hub page look like the default
        SCP theme, since the Hub has no styles.css of its own. A token that only
        an Asset Manager component reads (--sidebar-*, --scroll-shadow-color) is
        here too — a variable declaration is harmless on a page that never reads
        it, and the Appearance control below reads the --sidebar-* ones.
     2. THE LIGHT/DARK SWITCH (.theme-switch*), from the same file.
     3. THE APPEARANCE CONTROL'S STRUCTURE — backlog T1. It lived unscoped in
        themes/cyberpunk.css and duplicated in themes/apple.css; it lives here
        once now, and the theme files keep colour and material only.
     4. THE HUB MENUS (.topbar-hub, .hubmenu*, .hub-avatar*, .hub-app-tile*,
        .hub-account-*), from the "Hub chrome, VENDORED" block at the end of the
        Asset Manager's styles.css, which was itself a hand-kept copy of the
        Hub's hub.css.

   Everything here is UNSCOPED base — no [data-palette] selector, no !important
   (test/themes.test.js refuses both) — so every theme file, loading later and
   scoped to its own :root[data-palette="x"], wins on source order or on
   specificity exactly as it did when these rules lived in styles.css.
   =========================================================================== */

/* ===========================================================================
   1. THE SCP DESIGN TOKENS — the default (Street Culture) theme, as variables.
   The triple-block pattern every theme file also follows: the light values on
   :root:not([data-theme="dark"]), the dark values twice — once under
   prefers-color-scheme for a browser that has not chosen, once on
   :root[data-theme="dark"] for the explicit toggle — kept in step by hand.
   =========================================================================== */
:root {
  /* Pico's fluid type scale climbs to 125% on wide screens, which is too big;
     this pins it instead of letting it float with the viewport.
     100% = 1rem = 16px, the browser default and the size Pico's own spacing
     scale is designed around. It was 87.5% (14px) for two rounds, which made
     every rem-derived size in the framework — line-height, form-control
     padding, table cell padding, the whole type ramp — 12.5% smaller than
     Pico assumes, on a tool somebody reads at a desk all day. 14px is fine
     for a marketing page skimmed once; it is not fine for a 394-row
     inventory table. Anything rem-based scales with this, so raising it is
     the single-line version of increasing every text size in the app.
     Verified at 1440px against the real 394-asset import in both themes:
     the sidebar (fixed 224px), the assets/check-outs/assignees tables and
     the multi-section asset form all still fit without horizontal overflow. */
  --pico-font-size: 100%;
  --pico-border-radius: 0.5rem;
  --pico-font-family: 'Karla', var(--pico-font-family-sans-serif);
  --heading-font-family: 'Outfit', var(--pico-font-family-sans-serif);

  /* Fixed dark sidebar chrome — deliberately the same in light and dark
     mode, like most sidebar-nav apps (VS Code, Linear, Notion). */
  --sidebar-bg: #232324;
  --sidebar-ink: #eeeeef;
  --sidebar-muted: #8a8a8c;
  --sidebar-link: #cdcdce;
  --sidebar-link-active: #ffffff;
  --sidebar-hover-bg: #353536;
  --sidebar-active-bg: #00838f;

  /* Edge fade on horizontally-scrollable tables (see the overflow-x rule
     further down). Unlike the zebra-stripe overlay, this one can't be a
     single theme-agnostic alpha: it has to read as "content continues past
     this edge" against the page ground, and the dark ground (#1a1714) is
     already so close to black that a black gradient over it is invisible.
     Defined here rather than only inside the dark blocks so the token
     always has a value. */
  --scroll-shadow-color: rgba(0, 0, 0, 0.32);

  /* OPTIONAL UI SOUND (X1, 2026-09-04) for Street Culture, the DEFAULT theme —
     declared here, unscoped, for the same reason the rest of this block is:
     the default palette is the ABSENCE of [data-palette], so its own tokens
     have nowhere else to live. Before this the default theme made no sound
     at all and hid the Console-sound control, which is the worst place for
     that gap to be since it is what most people see first. The CHIME voice,
     reused rather than a new one built for this theme — see uiSound.js and
     material.css's identical note. Every theme that must stay silent for now
     (matrix, halloween — building them a voice is separate, open creative
     work) declares --theme-sound: 0 explicitly in its OWN file rather than
     relying on an absence, because this rule means "sound" is now the
     default an unscoped theme inherits unless it opts out. */
  --theme-sound: 1;
  --theme-voice: chime;

  /* AMBIENT MOTION — does this theme animate something on its own, with nobody
     touching it? Only two do: matrix's falling glyphs and halloween's candle
     flicker. Default 0, the opposite of --theme-sound above, because ambient
     motion is the exception among these themes and a theme that gains some
     should have to say so — the Appearance panel's Motion control is hidden
     unless this is 1, on the same reasoning as Sound's ("offering a switch that
     does nothing would be a control that lies").

     NOT about transitions or one-shot entrances. A dialog that slides in over
     0.16s is showing you what changed and is not what anybody means by "turn
     the animations off". */
  --theme-motion: 0;

  /* ---- semantic colour that is the SAME in light and dark -----------------
     These sit on the plain :root and not in the light block below, and that is
     load-bearing rather than tidy: :root:not([data-theme="dark"]) does not
     match an explicit dark toggle, so a token declared only there is UNDEFINED
     for anyone who has actually chosen dark — and var() with no fallback then
     resolves to nothing at all, which for a border-color silently becomes
     currentColor. Anything with no dark counterpart belongs here.

     The filter chip's filled state and the outline of an alerting one. Chrome,
     not status: this is the theme's own accent, which is why it was the most
     visible of the seven brand literals — it was SCP teal on all seven themes
     and both variants, measured. */
  --sem-chip-fill: #00838f;
  --sem-chip-ink: #ffffff;
  --sem-chip-alert-ink: #ffffff;
  --sem-chip-alert-fill: #a94c39;
  --sem-chip-alert-outline: #a94c39;

  /* THE ERROR FAMILY IS AN ALIAS OF THE BAD TIER, AND IT HAS TO BE AN ALIAS
     RATHER THAN THE SAME TOKEN. On this palette "a thing is in maintenance"
     and "this failed" are one salmon, and the file says so — the alert
     banner’s comment reads "reuses the same salmon trouble tone as the
     in_maintenance/lost status badges". A theme is entitled to disagree, and
     one already does: Electricity pitches in_maintenance as a calm arc blue
     (an asset at the shop is not a fault) and keeps red for real failure. Fold
     the two together and its error banner turns blue. So the error family
     points AT the bad tier by default and a theme can move it on its own.
     Read by [role="alert"], .import-error-list and the alerting filter chip. */
  --sem-error-bg: var(--sem-bad-bg);
  --sem-error-fg: var(--sem-bad-fg);
  --sem-error-border: var(--sem-bad-border);

  /* The rule down the side of a panel reporting an import error, and the
     salmon the error row tint is mixed from. */
  --sem-bad-accent: #d4573f;

  /* The table zebra stripe. An OVERLAY, not a colour: it composites onto
     whatever ground the theme is using, which is why it is one alpha rather
     than a light value and a dark one. The base is a warm brown tuned for the
     cream ground — over a cool page it reads as a brown wash across 394 rows,
     which is how it was found. */
  --zebra-stripe: rgba(120, 100, 70, 0.07);
}

/* ---------------------------------------------------------------------------
   THE SEMANTIC TIERS — one place for "what does good / caution / bad look
   like", so a theme re-pitches them ONCE instead of restating a component list.

   WHY THIS EXISTS, measured 2026-09-05 by reading getComputedStyle across all
   seven themes in both variants rather than by looking at the files. The Asset
   Manager's styles.css carried the SCP brand palette as LITERALS on nine
   families, each stated three times (light, prefers-color-scheme:dark, explicit
   [data-theme="dark"]). Themes could only reach them by restating all three
   blocks per selector, and mostly did not — so on the measurement:

     .import-outcome-* / .import-state-*   brand teal/amber in 7 themes of 7
     .audit-flag                            brand amber      in 7 of 7
     .filter-chip-active / -alert           brand teal/salmon in 7 of 7
     .flash-success / [role="alert"]        brand teal/salmon in 5 of 7
     .chart-bar-*                           brand palette    in 4 of 7
     the table zebra stripe                 warm brown       in 4 of 7

   Six themes were wearing a palette that belongs to the seventh, and four
   per-theme overrides would not have closed it: the real count is nine
   families x up to three cascade blocks x six themes. The failure the
   triplication causes is already on record in the backlog — a theme override
   that restates two of the three blocks "half-applies", and which half you see
   depends on whether the reader picked dark or their OS did.

   The values below are the Asset Manager's own, unchanged, so the default
   theme renders identically; what changes is that they are now reachable. The
   component rules that read them live in that app's styles.css, and the tokens
   live here because this file is where a token with no [data-palette] belongs
   (the default theme IS the absence of that attribute).

   ONE deliberate value change came with the move: the "bad" ink was two
   near-identical salmons, #914131 on the badges and #7a3527 on the error
   banner. They are one token now, at the darker of the two, which takes
   .status-in_maintenance / .status-rejected from 5.68:1 to 7.21:1 on their own
   fill and brings the tier into line with good (7.11) and caution (6.95).

   A theme re-pitches a tier by declaring these in its own :root block — see
   themes/README.md. It does NOT have to restate them per component, and it no
   longer has to remember that there are three cascade blocks.
   --------------------------------------------------------------------------- */
:root:not([data-theme="dark"]) {
  /* good — available, approved, received, a successful flash, a created row */
  --sem-good-bg: #e6f8fa;
  --sem-good-fg: #005b68;
  --sem-good-border: #b8e9ee;

  /* caution — checked out, pending, ordered, a draft batch, the audit flag */
  --sem-caution-bg: #ffeab8;
  --sem-caution-fg: #664a07;
  --sem-caution-border: #e8c777;

  /* bad — in maintenance, rejected, the error banner */
  --sem-bad-bg: #fce2dc;
  --sem-bad-fg: #7a3527;
  --sem-bad-border: #f2b7a8;

  /* alarm — one step past bad: lost, and a row the importer refused */
  --sem-alarm-bg: #f9b6a9;
  --sem-alarm-fg: #612b21;

  /* neutral — closed, cancelled, skipped: real states with nothing to act on */
  --sem-neutral-bg: #dedede;
  --sem-neutral-fg: #353536;

  /* two more greys the status vocabulary already distinguishes: not working
     (present but out of service) and donated (gone from the inventory) */
  --sem-inactive-bg: #eeeeef;
  --sem-inactive-fg: #464648;
  --sem-archived-bg: #cdcdce;
  --sem-archived-fg: #232324;

  /* Dashboard chart-bar marks. A separate set from the status tiers above and
     not derived from them, because these are read as a SOLID FILL rather than
     as text on a pill.
     THE BACKDROP THAT MATTERS IS THE TRACK, NOT THE PAGE — corrected
     2026-09-10. This comment said "contrast-checked against the page ground",
     and the bar sits INSIDE `.chart-track`, so the track is what decides
     whether you can see where the bar ends. Measured against the real
     composited track, every light theme failed WCAG 1.4.11's 3:1: street
     culture 2.86, matrix 2.47, cyberpunk 2.41, halloween 2.32, apple 2.03,
     material 2.02, electricity 1.02 — SEVEN of seven, not the five an earlier
     sweep reported.
     The fills are unchanged. `--chart-bar-edge` below is what closes it. */
  --chart-bar-available: #007f92;
  --chart-bar-checked_out: #b2820d;
  --chart-bar-in_maintenance: #c25642;
  --chart-bar-not_working: #8a8a8c;
  --chart-bar-lost: #914131;
  --chart-bar-disposed: #58585a;
  --chart-bar-donated: #353536;
  --chart-bar-category: #007f92;

  /* THE BAR'S EDGE, and it is why none of the fills above had to move.
     WCAG 1.4.11 asks for a visible BOUNDARY, and what a reader needs from this
     chart is where the bar ENDS — so a hairline in a flat overlay does the job
     that re-pitching eight colours per theme would have done.
     Two alternatives were measured and rejected. LIGHTENING THE TRACK cannot
     work: Electricity's lightest bar needs a track luminance of 2.12 and white
     is 1.0, so no track fixes it, and for the other six the track ends up
     within 1.02-1.15 of the page — trading an invisible bar end for an
     invisible track. RE-PITCHING THE BARS is a taste call across seven themes
     for a problem that is not about hue.
     A flat overlay rather than a per-theme colour, so a theme re-pitched later
     needs no recomputation: measured across all seven themes x eight bars,
     black at 0.45 clears 3:1 everywhere in light mode (worst 3.08) and white
     at 0.15 in dark (worst 3.29). Set a little above each for headroom. */
  --chart-bar-edge: rgba(0, 0, 0, 0.5);
}

:root:not([data-theme="dark"]) {
  /* TEXT teal, darkened one step 2026-09-04 (backlog X11). --pico-primary is
     what links, outline buttons and focus rings are drawn in; the brand teal
     the filled buttons wear is --pico-primary-background below, which is
     untouched. Measured with the WCAG formula against the real surfaces:
     #007f92 was 4.45:1 on the #fbf8f2 ground and 4.07:1 on a zebra-striped
     table row — under AA on the most common text in the app (every asset
     name and holder name in every list is one of these links). #007487 is
     5.15:1 and 4.71:1. A white ground would not have fixed it (4.72:1, and
     still ~4.4:1 on its zebra rows); the ink was the lever, not the paper.
     Hover (#006d7d) is still the darker of the two, so hovering still darkens. */
  --pico-primary: #007487;
  --pico-primary-background: #007f92;
  --pico-primary-underline: rgba(0, 127, 146, 0.5);
  --pico-primary-hover: #006d7d;
  --pico-primary-hover-background: #006d7d;
  --pico-primary-focus: rgba(0, 181, 208, 0.5);
  --pico-primary-inverse: #fff;

  --pico-secondary: #58585a;
  --pico-secondary-background: #58585a;
  --pico-secondary-hover: #464648;
  --pico-secondary-hover-background: #464648;
  --pico-secondary-focus: rgba(88, 88, 90, 0.25);
  --pico-secondary-inverse: #fff;

  --pico-mark-background-color: #ffdd89;
  --pico-mark-color: #4c3805;

  /* THE GROUND IS A DECISION, NOT A DEFAULT (backlog X11, 2026-09-04). A warm
     near-white is the single most common signature of generated design, so it
     was re-decided rather than inherited. Candidates rendered and measured on
     2026-09-04: this cream, a warm off-white (#faf9f6), a neutral paper
     (#f6f6f4) and Pico's own white. On body text they are within 0.6:1 of
     each other (10.3–11.1:1) and on muted text within 0.4 (4.5–4.8:1), so
     there was no legibility case to decide on; the case is the brand. Street
     Culture's palette is warm — amber #feba12 and salmon #f26c52 beside the
     teal — and the status pills built from it (amber "checked out", salmon
     "lost") sit on this paper as one family, where on white they read as
     web defaults. The sidebar is near-black (#232324), so the content area is
     paper against ink, which suits an inventory tool used at a desk all day
     better than glare-white. The one cost the cream did carry — the text
     teal at 4.45:1 — is paid at --pico-primary above, not by the ground. This
     is what Pico's own body{background} rule paints. The dark ground below is
     its own decision and untouched. */
  --pico-background-color: #fbf8f2;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --pico-primary: #4dcbde;
    --pico-primary-background: #007f92;
    --pico-primary-underline: rgba(77, 203, 222, 0.5);
    --pico-primary-hover: #80dae8;
    --pico-primary-hover-background: #0091a6;
    --pico-primary-focus: rgba(77, 203, 222, 0.375);
    --pico-primary-inverse: #fff;

    --pico-secondary: #acacad;
    --pico-secondary-background: #58585a;
    --pico-secondary-hover: #cdcdce;
    --pico-secondary-hover-background: #69696b;
    --pico-secondary-focus: rgba(172, 172, 173, 0.25);
    --pico-secondary-inverse: #fff;

    --pico-mark-background-color: #7f5d09;
    --pico-mark-color: #fff8e7;

    /* Warm-tinted dark ground, so the "paper" identity carries into dark
       mode instead of falling back to Pico's cool near-black. */
    --pico-background-color: #1a1714;

    /* Light, not darker: there's no headroom left below #1a1714 for a black
       gradient to register, so the fade goes the other direction here. */
    --scroll-shadow-color: rgba(255, 255, 255, 0.16);

    /* THE SEMANTIC TIERS, dark half. Keep in step with the explicit-toggle
       copy in :root[data-theme="dark"] below — that pair has drifted twice in
       the theme files and the failure is invisible to whoever makes it. */
    --sem-good-bg: #00363e;
    --sem-good-fg: #99e1ec;
    --sem-good-border: #005b68;

    --sem-caution-bg: #4c3805;
    --sem-caution-fg: #ffe3a0;
    --sem-caution-border: #6d5209;

    --sem-bad-bg: #492019;
    --sem-bad-fg: #f9b6a9;
    --sem-bad-border: #6b2e22;

    --sem-alarm-bg: #612b21;
    --sem-alarm-fg: #fac4ba;

    --sem-neutral-bg: #2c2c2d;
    --sem-neutral-fg: #acacad;

    --sem-inactive-bg: #353536;
    --sem-inactive-fg: #cdcdce;
    --sem-archived-bg: #1a1a1b;
    --sem-archived-fg: #9b9b9c;

    --chart-bar-available: #00b5d0;
    --chart-bar-checked_out: #feba12;
    --chart-bar-in_maintenance: #f26c52;
    --chart-bar-not_working: #bcbcbd;
    --chart-bar-lost: #f58975;
    --chart-bar-disposed: #cdcdce;
    --chart-bar-donated: #eeeeef;
    --chart-bar-category: #4dcbde;
    /* White on dark, for the same reason black is used on light: the edge has
       to move AWAY from the track's luminance. Darkening a bar on a dark track
       moves it toward the track, which is why electricity/dark was the one
       combination a darken-only rule could not solve. */
    --chart-bar-edge: rgba(255, 255, 255, 0.2);
  }
}

:root[data-theme="dark"] {
  --pico-primary: #4dcbde;
  --pico-primary-background: #007f92;
  --pico-primary-underline: rgba(77, 203, 222, 0.5);
  --pico-primary-hover: #80dae8;
  --pico-primary-hover-background: #0091a6;
  --pico-primary-focus: rgba(77, 203, 222, 0.375);
  --pico-primary-inverse: #fff;

  --pico-secondary: #acacad;
  --pico-secondary-background: #58585a;
  --pico-secondary-hover: #cdcdce;
  --pico-secondary-hover-background: #69696b;
  --pico-secondary-focus: rgba(172, 172, 173, 0.25);
  --pico-secondary-inverse: #fff;

  --pico-mark-background-color: #7f5d09;
  --pico-mark-color: #fff8e7;
  --pico-background-color: #1a1714;

  /* Keep in step with the prefers-color-scheme:dark block above — this is
     the explicit-toggle half of the same pair. */
  --scroll-shadow-color: rgba(255, 255, 255, 0.16);

  /* THE SEMANTIC TIERS, dark half — the explicit-toggle copy. */
  --sem-good-bg: #00363e;
  --sem-good-fg: #99e1ec;
  --sem-good-border: #005b68;

  --sem-caution-bg: #4c3805;
  --sem-caution-fg: #ffe3a0;
  --sem-caution-border: #6d5209;

  --sem-bad-bg: #492019;
  --sem-bad-fg: #f9b6a9;
  --sem-bad-border: #6b2e22;

  --sem-alarm-bg: #612b21;
  --sem-alarm-fg: #fac4ba;

  --sem-neutral-bg: #2c2c2d;
  --sem-neutral-fg: #acacad;

  --sem-inactive-bg: #353536;
  --sem-inactive-fg: #cdcdce;
  --sem-archived-bg: #1a1a1b;
  --sem-archived-fg: #9b9b9c;

  --chart-bar-available: #00b5d0;
  --chart-bar-checked_out: #feba12;
  --chart-bar-in_maintenance: #f26c52;
  --chart-bar-not_working: #bcbcbd;
  --chart-bar-lost: #f58975;
  --chart-bar-disposed: #cdcdce;
  --chart-bar-donated: #eeeeef;
  --chart-bar-category: #4dcbde;
  /* Declared in ALL THREE blocks, which is this file's own convention and not
     ceremony: a token set only under `@media (prefers-color-scheme: dark)` is
     silently absent for somebody who CHOSE dark in the Appearance control.
     Measured while getting this wrong — the edge stayed black on dark and every
     theme sat at 1.06-1.71 against the track. */
  --chart-bar-edge: rgba(255, 255, 255, 0.2);
}

/* ===========================================================================
   2. THE LIGHT/DARK SWITCH — an actual sliding toggle (track + thumb), not a
   button that swaps an emoji glyph, so it reads as a real setting control.
   Colors come from the theme-independent --sidebar-* tokens by default, since
   its first home was the Asset Manager's dark sidebar; inside the Appearance
   panel (section 3) it is re-pointed at the panel's own surface, and the
   .theme-switch-page modifier swaps in the page's own light/dark-aware --pico-*
   tokens. Moved verbatim from the Asset Manager's styles.css.
   =========================================================================== */
.theme-switch {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  padding: 0.3rem 0.6rem 0.2rem;
  cursor: pointer;
  position: relative;
}

.theme-switch input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.theme-switch-icon {
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
  color: var(--sidebar-muted);
  transition: color 0.18s ease;
}

.theme-switch:has(input:checked) .theme-switch-icon:first-of-type {
  color: var(--sidebar-muted);
}
.theme-switch:has(input:checked) .theme-switch-icon:last-of-type,
.theme-switch:not(:has(input:checked)) .theme-switch-icon:first-of-type {
  color: var(--sidebar-link-active);
}

.theme-switch-track {
  position: relative;
  width: 2.5rem;
  height: 1.4rem;
  border-radius: 999px;
  background: var(--sidebar-hover-bg);
  flex-shrink: 0;
  transition: background-color 0.18s ease;
}

.theme-switch-thumb {
  position: absolute;
  top: 0.15rem;
  left: 0.15rem;
  width: 1.1rem;
  height: 1.1rem;
  border-radius: 50%;
  background: var(--sidebar-muted);
  transition: transform 0.18s ease, background-color 0.18s ease;
}

.theme-switch input:checked ~ .theme-switch-track .theme-switch-thumb {
  transform: translateX(1.1rem);
  background: var(--sidebar-active-bg);
}

.theme-switch input:focus-visible ~ .theme-switch-track {
  outline: 2px solid var(--sidebar-active-bg);
  outline-offset: 2px;
}

.theme-switch-page .theme-switch-icon {
  color: var(--pico-muted-color);
}
.theme-switch-page:has(input:checked) .theme-switch-icon:last-of-type,
.theme-switch-page:not(:has(input:checked)) .theme-switch-icon:first-of-type {
  color: var(--pico-color);
}
.theme-switch-page .theme-switch-track {
  background: var(--pico-secondary-background);
}
.theme-switch-page .theme-switch-thumb {
  background: var(--pico-muted-color);
}
.theme-switch-page input:checked ~ .theme-switch-track .theme-switch-thumb {
  background: var(--pico-primary);
}
.theme-switch-page input:focus-visible ~ .theme-switch-track {
  outline: 2px solid var(--pico-primary);
  outline-offset: 2px;
}

/* ===========================================================================
   3. THE APPEARANCE CONTROL — structure AND the default look
   (views/chrome/appearance.ejs; behaviour in public/js/themeToggle.js).

   A single ~32px trigger and a floating panel. In the Asset Manager the trigger
   sits at the foot of the sidebar: four earlier versions put the controls
   themselves in the column (133px stacked, ~100px in two pinned rows, a one-row
   version that truncated the theme name) and the controls are set rarely and do
   not earn permanent furniture in a navigation column. On the Hub, and on any
   page with no sidebar, the `.appearance-page` variant at the end of this
   section hangs the panel from the top-right instead.

   BACKLOG T1, RESOLVED HERE (2026-09-02). Until now these rules lived UNSCOPED
   in themes/cyberpunk.css — the first theme built — and were DUPLICATED, scoped,
   in themes/apple.css, so the control had no base at all: any base fix lost the
   cascade to the scoped copy, and three themes that styled none of this were
   borrowing cyberpunk's structure by accident. This block is that structure,
   once. A theme file may re-pitch what the control LOOKS like (background,
   border, radius, shadow, material, type) and nothing about where it is or how
   big it is — test/structure.test.js refuses a theme that declares position,
   display, sizing, offsets, z-index, padding, margin, gap or list-style on any
   .appearance* or .theme-switch* selector. The colours below are the DEFAULT
   look (Street Culture), drawn from the tokens in section 1, so under a theme
   they are simply what the theme did not override.
   =========================================================================== */
.appearance {
  position: relative;
  /* Pico gives every <details> a bottom margin and every OPEN summary another
     one (details[open] > summary { margin-bottom: var(--pico-spacing) }). In
     a topbar that is a 16px jump in the bar's height the moment the control
     opens — measured on the Hub 2026-09-02, 62px closed, 78px open — the same
     class of shift the Asset Manager fixed on its sign-in page on 2026-09-01.
     The panel is position: fixed and takes no space; these margins were the
     whole shift. */
  margin-bottom: 0;
}
.appearance[open] > .appearance-trigger {
  margin-bottom: 0;
}

/* Matches .sidebar-nav a exactly, so it reads as part of the column rather than
   a widget bolted underneath it. */
/* `.appearance .appearance-trigger` and not `.appearance-trigger` alone: Pico
   styles `summary` with its own colour rule, which won on specificity and left
   this text at light-mode body colour ON THE DARK SIDEBAR — measured at 1.41:1
   contrast against the sidebar background, versus 9.88:1 for the nav links
   directly above it. Effectively invisible, and invisible in a way that only
   shows up in the light theme, because in dark mode the inherited colour
   happens to be light. Matched to the nav links rather than made brighter: this
   is a peer of Dashboard and Assets, not something that should out-shout them. */
.appearance .appearance-trigger {
  color: var(--sidebar-link);
}

/* NO `display` OVERRIDE ON THE <summary> ITSELF — the flex row lives on
   .appearance-trigger-inner below. See views/chrome/appearance.ejs for the
   reasoning; the short version is that a summary whose `display` is changed
   leaves the UA's own summary render path, which is where engines diverge,
   and Pico (whose details.dropdown drives the working Export/Columns menus in
   this app) never overrides it either. */
.appearance-trigger {
  padding: 0.42rem 0.6rem;
  border-radius: 0.5rem;
  color: var(--sidebar-link);
  font-size: 0.88rem;
  font-weight: 500;
  cursor: pointer;
  /* The UA draws a disclosure triangle on every <summary>; all three removal
     paths below are needed, and they only WORK because the `display` override
     is gone. The HTML spec's UA stylesheet is
     `details > summary:first-of-type { display: list-item; list-style:
     disclosure-closed inside }` — so `list-style: none` and `::marker` suppress
     the marker by acting on a LIST ITEM. Force `display: flex` (as this rule
     used to) and the summary is not a list item at all, those two declarations
     have nothing to act on, and suppression rests entirely on WebKit's legacy
     `::-webkit-details-marker`. Staying on the standard path is what makes the
     suppression portable rather than dependent on one engine's private
     pseudo-element.
     NOTE: the chevron that remains, and that rotates from > to v on open, is
     NOT the UA marker — it is Pico's own `details summary::after` (a
     background-image chevron, `float: right`, rotated -90deg when closed and 0
     when open). It is meant to be there. */
  list-style: none;
}

/* INLINE-flex, not flex, and `vertical-align: top` — both measured, neither is
   taste. Pico draws its disclosure chevron as `summary::after { float: right }`.
   A right float cannot share a line with a full-width BLOCK box, so a
   block-level `display: flex` here pushed the chevron onto its own line below
   the pill. Inline-level keeps it on the row where it has always been.
   `vertical-align: top` (rather than the obvious `middle`, or the initial
   `baseline`) is what makes the trigger exactly the height it was before this
   wrapper existed: measured at 29.4375px for top/bottom, 30.54 for middle and
   32.94 for baseline, against 29.4375 for the original flex summary. */
.appearance-trigger-inner {
  display: inline-flex;
  vertical-align: top;
  align-items: center;
  gap: 0.5rem;
}

.appearance-trigger::-webkit-details-marker {
  display: none;
}

.appearance-trigger::marker {
  content: '';
}

.appearance-trigger svg {
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
  opacity: 0.8;
}

.appearance-trigger:hover,
.appearance[open] .appearance-trigger {
  background: var(--sidebar-hover-bg);
  color: var(--sidebar-link-active);
}

/* FIXED, not absolute, and this is a requirement rather than a preference:
   the sidebar's scrolling half (`.sidebar-scroll`) is `overflow-y: auto`, which
   CLIPS absolutely-positioned descendants, so a panel anchored to the trigger
   would be cut off at the column's edge. Fixed escapes the clip, and lets the
   panel be wider than the 224px column it opens from — which is what makes the
   theme name fit instead of truncating to "SCP Colour⌄". Positioned off the
   sidebar's own width so it sits just outside the column, level with the
   trigger at the bottom.
   THE OTHER HALF OF MAKING THAT WORK IS IN THE ASSET MANAGER'S styles.css: the
   overflow lives on `.sidebar-scroll` and NOT on `.sidebar`, because WebKit
   clips a fixed descendant to a scrollable ancestor and this panel renders
   outside the column — see the `.sidebar-scroll` comment there before changing
   either. The `left: calc(224px + 0.5rem)` is that sidebar's width; a host with
   no sidebar uses the `.appearance-page` variant below and never sees it. */
.appearance-panel {
  position: fixed;
  left: calc(224px + 0.5rem);
  bottom: 1rem;
  z-index: 60;
  width: 15rem;
  padding: 0.9rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  border: 1px solid var(--pico-card-border-color);
  border-radius: 0.6rem;
  background: var(--pico-card-background-color);
  color: var(--pico-color);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}

.appearance-field {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin: 0;
}

.appearance-field-row {
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
}

.appearance-field-label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--pico-muted-color);
}

.appearance-panel select {
  margin: 0;
  padding: 0.4rem 1.75rem 0.4rem 0.55rem;
  font-size: 0.88rem;
  height: auto;
}

/* The switch defaults to the --sidebar-* palette because its original home was
   the dark sidebar. In here it sits on a card, which in Street Culture light would
   make it near-invisible, so it is re-pointed at the panel's own surface. */
.appearance-panel .theme-switch {
  margin: 0;
}

.appearance-panel .theme-switch-icon {
  color: var(--pico-muted-color);
}

/* X3, 2026-09-04: the sound level select and the Preview button share ONE
   [data-sound-field] wrapper (see appearance.ejs's comment on why there is
   only one), so this gives that wrapper the same column rhythm as the rest
   of the panel rather than letting its two rows collapse together. */
[data-sound-field] {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

/* The Preview button's own row — a single element, not a label/control pair,
   so it does not reuse .appearance-field-row (built for the space-between
   layout the two-child rows above it need). */
.appearance-preview-row {
  display: flex;
  justify-content: flex-end;
}

.appearance-note {
  margin: 0;
  font-size: 0.75rem;
  line-height: 1.4;
  color: var(--pico-muted-color);
}

/* The sidebar is a slide-in drawer below this width, and a panel positioned off
   a 224px column would land in the wrong place — so it anchors to the left edge
   instead and narrows to fit a phone. */
@media (max-width: 768px) {
  .appearance-panel {
    left: 0.75rem;
    right: 0.75rem;
    width: auto;
  }
}

/* On the signed-out /login page there is no sidebar to open from, so the trigger
   sits in the page's own top strip and the panel hangs from the top-right. */
.appearance-page .appearance-trigger {
  color: var(--pico-muted-color);
}

.appearance-page .appearance-panel {
  left: auto;
  right: 1rem;
  top: 3.5rem;
  bottom: auto;
}

/* The strip a signed-out page gives the page-variant control to sit in: a flex
   row pushed to the right, the same height as a topbar, so the <details> is
   sized to its own label instead of stretching across the page (measured on
   the Hub sign-in page 2026-09-02: 1280px wide, label at the far left, chevron
   at the far right). Shared here because both apps render the same strip for
   the same reason; it used to live only in the Asset Manager's styles.css. */
.pre-auth-topbar {
  display: flex;
  justify-content: flex-end;
  padding: 0.85rem 2rem 0;
  height: 3.7rem;
}

/* ===========================================================================
   4. THE HUB MENUS — the waffle app-switcher and the account menu
   (views/chrome/switcher.ejs and account.ejs, behaviour in public/js/menus.js).
   Pitched entirely on Pico's tokens so every theme re-colours them for free,
   and prefixed hub-/hubmenu- so nothing collides with Pico or a theme file.
   Moved verbatim from the end of the Asset Manager's styles.css, where it was
   a hand-kept copy of the Hub's own rules; the two are one block now.
   =========================================================================== */
.topbar-hub { display: flex; align-items: center; gap: 0.25rem; margin-left: 0.75rem; }
.hubmenu { position: relative; }
.hubmenu-icon-button,
.hubmenu-avatar-button {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; padding: 0; margin: 0; border: 0; border-radius: 50%;
  background: transparent; color: var(--pico-muted-color); box-shadow: none; cursor: pointer;
}
.hubmenu-icon-button:hover, .hubmenu-avatar-button:hover,
.hubmenu-icon-button:focus-visible, .hubmenu-avatar-button:focus-visible {
  background: color-mix(in srgb, currentColor 10%, transparent); color: var(--pico-color);
}
.hub-avatar {
  display: inline-flex; align-items: center; justify-content: center;
  /* flex: 0 0 auto — in a flex row beside a long email the circle would shrink
     into an egg (the Hub's own avatar did exactly that, 2026-09-01). */
  flex: 0 0 auto;
  width: 32px; height: 32px; border-radius: 50%;
  background: var(--pico-primary-background); color: var(--pico-primary-inverse);
  font-weight: 600; font-size: 0.9rem; line-height: 1;
}
.hub-avatar-lg { width: 44px; height: 44px; font-size: 1.15rem; }
.hubmenu-popover {
  position: absolute; right: 0; top: calc(100% + 8px); z-index: 30;
  min-width: 240px; padding: 0.5rem;
  background: var(--pico-card-background-color);
  border: 1px solid var(--pico-muted-border-color);
  border-radius: var(--pico-border-radius);
  box-shadow: var(--pico-card-box-shadow);
}
.hubmenu-popover-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.25rem; width: 300px; }
.hub-app-tile {
  display: flex; flex-direction: column; align-items: center; gap: 0.35rem;
  padding: 0.8rem 0.4rem; border-radius: 10px;
  color: var(--pico-color); text-decoration: none; text-align: center;
}
.hub-app-tile:hover, .hub-app-tile:focus-visible {
  background: color-mix(in srgb, currentColor 8%, transparent); text-decoration: none;
}
.hub-app-tile.is-current { outline: 2px solid var(--pico-primary-background); outline-offset: -2px; }
.hub-app-tile-icon { font-size: 1.6rem; line-height: 1; }
.hub-app-tile-name { font-size: 0.72rem; font-weight: 500; line-height: 1.2; }
.hubmenu-popover-account { min-width: 260px; padding: 0.25rem; }
.hub-account-head {
  display: flex; align-items: center; gap: 0.65rem; padding: 0.75rem;
  border-bottom: 1px solid var(--pico-muted-border-color); margin-bottom: 0.25rem;
}
.hub-account-who { display: flex; flex-direction: column; align-items: flex-start; gap: 0.3rem; min-width: 0; }
.hub-account-email { font-size: 0.9rem; color: var(--pico-muted-color); overflow-wrap: anywhere; }
.hubmenu-item-form { margin: 0; }
.hubmenu-item-button {
  display: block; width: 100%; margin: 0; padding: 0.6rem 0.75rem;
  border: 0; border-radius: 8px; background: transparent; box-shadow: none;
  /* Pico sets a button's colour through its OWN --pico-color (the primary
     inverse, i.e. white), so var(--pico-color) here resolved to white-on-white
     — measured 2026-09-01, the label was invisible. Inherit both instead. */
  --pico-color: inherit; color: inherit; font: inherit; font-size: 0.92rem; text-align: left; cursor: pointer;
}
.hubmenu-item-button:hover, .hubmenu-item-button:focus-visible {
  background: color-mix(in srgb, currentColor 8%, transparent);
}
/* The two items the Hub renders that the Asset Manager's copy never had: the
   "Hub settings & admin" link (an <a>, styled like the Sign out button beside it
   so the menu reads as one list), and the empty state for a person with a Hub
   account and no grants yet. Both came from the Hub's old hub.css, re-pitched
   onto Pico's tokens and renamed onto the hubmenu- prefix. */
.hubmenu-item {
  display: block; padding: 0.6rem 0.75rem; border-radius: 8px;
  color: inherit; text-decoration: none; font-size: 0.92rem;
}
.hubmenu-item:hover, .hubmenu-item:focus-visible {
  background: color-mix(in srgb, currentColor 8%, transparent); text-decoration: none;
}
.hubmenu-empty { grid-column: 1 / -1; margin: 0; padding: 0.5rem; color: var(--pico-muted-color); text-align: center; }

/* The role badge account.ejs renders when an app passes `role`. Also used by the
   Asset Manager's own /users table and its standalone topbar, which is why it
   lives with the chrome and not in that app's stylesheet. */
.role-badge {
  display: inline-block;
  padding: 0.1rem 0.55rem;
  border-radius: 999px;
  font-size: 0.75em;
  font-weight: 600;
  white-space: nowrap;
  background: var(--pico-secondary-background);
  color: var(--pico-secondary-inverse);
}

/* ===========================================================================
   5. THE FEEDBACK DIALOG (N2, 2026-09-04) — views/chrome/feedback.ejs, opened
   by public/js/feedback.js from the account menu's "Send feedback" item; the
   same .feedback-* form also renders on the Hub's own no-JS /feedback page.

   THE PANEL IS THE <article>, NOT THE <dialog>. Pico styles the dialog element
   itself as the full-viewport flex overlay that centres an article, so content
   placed directly in the dialog spans the whole screen — the Asset Manager
   measured 1085px on its confirmation dialog before adopting this shape, and
   its styles.css says so. Following Pico's structure is also what lets every
   theme colour this with no per-theme rule: structure here, colour from the
   same --pico-* tokens the menus above use. A theme may restyle the surface.
   =========================================================================== */
.feedback-dialog::backdrop {
  background: rgba(0, 0, 0, 0.5);
  /* Progressive: engines without backdrop-filter still get the dim. */
  backdrop-filter: blur(2px);
}

.feedback-dialog > article {
  max-width: min(34rem, calc(100vw - 2rem));
  /* A hairline ring as well as the shadow: on a dark ground a drop shadow
     occludes no light and disappears, so the ring is what separates the panel
     from the backdrop there (the confirmation dialog learned the same). */
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.4), 0 0 0 1px var(--pico-card-border-color, var(--pico-muted-border-color));
}

.feedback-form {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  margin: 0;
}

.feedback-title {
  margin: 0;
  font-size: 1.2rem;
  line-height: 1.25;
}

/* Two radios on one row. Pico gives fieldset, label and input their own
   bottom margins for stacked forms; this row is the one place they are reset. */
.feedback-kind {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  margin: 0;
  padding: 0;
  border: 0;
}
.feedback-kind label {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  margin: 0;
  cursor: pointer;
}
.feedback-kind input[type="radio"] { margin: 0; }

.feedback-prompt {
  margin: 0;
  font-weight: 600;
}

.feedback-form textarea {
  margin: 0;
  min-height: 7.5rem;
  resize: vertical;
}

.feedback-meta {
  margin: 0;
  font-size: 0.8rem;
  color: var(--pico-muted-color);
}

.feedback-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
}
.feedback-actions button,
.feedback-actions [role="button"] {
  width: auto;
  margin: 0;
}

/* Arrive with a small lift — off entirely under reduced motion, not faded. */
@media (prefers-reduced-motion: no-preference) {
  .feedback-dialog[open] > article {
    animation: feedback-dialog-in 0.16s ease-out;
  }
  @keyframes feedback-dialog-in {
    from { opacity: 0; transform: translateY(-0.5rem) scale(0.985); }
    to   { opacity: 1; transform: none; }
  }
}
