/* ============================================================
   Design Language v1 — foundation reset (2026-05-13)
   Updated: --bg, --surface, --surface-2, --border, --border-2, --text
   Added: --surface-chrome, --control-bg, --text-body, --text-preview,
          --text-meta, --text-disabled, --callout-bg, --callout-accent
   Alias: --muted → --text-meta (kept for back-compat)
   Note: --border remains a color token (not shorthand). 0.5px width
         sweep is a separate targeted pass.
   ============================================================ */
:root{
  color-scheme: dark;
  /* UI freeze tokens: typography + spacing should use these vars. */

  --bg: #061020;
  /* Dark mode: --surface-2 is the BASE surface (less white overlay = darker).
     --surface is the LIFTED state (more white overlay = lighter on hover).
     Reversed from what naming might suggest; light mode is naturally aligned. */
  /* Surfaces are OPAQUE, derived from --bg via color-mix, so the ENTIRE dark theme
     is driven by --bg alone: change it and every surface re-seats. Opaque (not
     translucent veils) means nothing bleeds through — no page wash, no scrolled
     content behind the fixed nav, no nesting compound — so every consumer is a plain
     `background: var(--surface*)` with zero per-surface patching. Elevation is just a
     lightness ladder: surface-2 (cards) < surface (hover/nav/footer) < surface-chrome
     (floating menus/modals). Hairlines + button fills (--border, --btn*) STAY
     translucent on purpose — they overlay surfaces and should relate to what they
     sit on. */
  /* The wash is INDIGO, not white (2026-08-13). White can only buy brightness by
     spending chroma: it adds the same amount to every channel, so the R-B spread
     that makes --bg navy stays fixed while the level climbs, and cards drain
     toward grey — that was the "no pop" ceiling. Mixing the base's own hue back
     in lifts AND holds the colour: at these percentages the cards keep 86-90% of
     the base saturation.

     The wash STAYS NEAR THE BASE HUE, and that constraint is load-bearing. A
     first pass used indigo #4152d6, which sits at 297.8deg — within ~6deg of the
     tier accents (Curator #A78BFA 303.7, Navigator #4F46E5 302.6). Surfaces and
     accent were then the same hue, so the accent had nothing to read against and
     the whole UI drifted violet, worst when switching tiers. #496fb8 is 282.4deg:
     5.8deg off the base, ~20deg clear of the accents. Keep any future wash near
     --bg's hue and well clear of the tier range, or the tier colour stops
     signalling.

     Judge these by dE*ab, NOT contrast ratio: contrast is luminance-only and
     blind to a hue shift, so it misreads a tinted surface as flatter than a
     paler one that is visibly less distinct. Cards sit 7.1 dE from the page
     (the white ladder this replaced: 7.8) while holding 83% of the base
     saturation, which white cannot do at any percentage. */
  --surface:   color-mix(in srgb, #496fb8 15%, var(--bg));
  --surface-2: color-mix(in srgb, #496fb8 11%, var(--bg));
  --surface-chrome: color-mix(in srgb, #496fb8 19%, var(--bg));
  --control-bg: rgba(255,255,255,0.08);

  --text: #E8EEF5;
  --text-body: #C4CFDC;
  --text-preview: #A8B5C7;
  --text-meta: #6B7D92;
  --text-disabled: #3A4A5F;
  --muted: var(--text-meta);

  /* .06 -> .09 with the ladder above: fill alone lifts a card but leaves it
     mushy — the crisper hairline is what makes the edge actually read. */
  --border: rgba(255,255,255,0.09);
  --border-2: rgba(255,255,255,0.12);

  --btn: rgba(255,255,255,0.08);
  --btn-hover: rgba(255,255,255,0.12);

  --accent-green: #16a34a;
  --accent-green-strong: #15803d;
  --accent-green-soft: rgba(22, 163, 74, 0.45);
  --accent-green-bg: rgba(22, 163, 74, 0.2);
  --accent-green-bg-hover: rgba(22, 163, 74, 0.3);

  --accent-orange: #f59e0b;
  --accent-orange-strong: #d97706;
  --accent-orange-soft: rgba(245, 158, 11, 0.5);

  --accent-red: #ef4444;
  --accent-red-soft: rgba(239, 68, 68, 0.5);

  --accent-blue: #0698ea;
  --accent-blue-soft: rgba(6, 152, 234, 0.4);
  --accent-blue-bg: rgba(6, 152, 234, 0.2);
  --accent-blue-bg-hover: rgba(6, 152, 234, 0.3);

  --callout-bg: rgba(33,150,243,0.06);
  --callout-accent: #2196F3;
  --callout-bg-processing: rgba(251,191,36,0.06);

  --layout-inset-desktop: 24px;
  --layout-inset-mobile: 20px;
  --content-max-width: 980px;
  --type-display-size: clamp(2rem, 4.8vw, 3.9rem);
  --type-display-line: 1.04;
  --type-display-track: -0.028em;
  --type-heading-size: clamp(1.05rem, 1.3vw, 1.2rem);
  --type-heading-line: 1.12;
  --type-heading-track: -0.01em;
  --type-detail-size: 1.375rem;
  --type-body-size: 0.8125rem;
  --type-preview-size: 0.75rem;
  --type-card-title-size: 0.875rem;
  --type-card-title-line: 1.15;
  --type-card-title-track: 0.09em;
  --type-meta-size: 0.6875rem;
  --type-meta-track: 0.08em;
  --type-label-size: 0.625rem;
  --type-foot-size: 0.49rem;
  --icon-btn-size: 40px;
  --icon-glyph-size: 18px;
  --icon-copy-size: var(--icon-btn-size);
  --icon-copy-glyph: var(--icon-glyph-size);

  --space-1: 4px;
  --space-2: 6px;
  --space-3: 8px;
  --space-4: 10px;
  --space-5: 12px;
  --space-6: 16px;

  --pill-done-fg:         #4ADE80;
  --pill-done-bg:         rgba(34, 197, 94, 0.15);
  --pill-processing-fg:   #FBBF24;
  --pill-processing-bg:   rgba(251, 191, 36, 0.15);
  --pill-failed-fg:       #F87171;
  --pill-failed-bg:       rgba(239, 68, 68, 0.15);
  --pill-queued-fg:       #A8B5C7;
  --pill-queued-bg:       rgba(107, 125, 146, 0.15);

  --tag-blue:             #64B5F6;
  --tag-blue-bg:          rgba(100, 181, 246, 0.10);
  --tag-purple:           #A78BFA;
  --tag-purple-bg:        rgba(167, 139, 250, 0.10);
  --tag-teal:             #5EEAD4;
  --tag-teal-bg:          rgba(94, 234, 212, 0.10);
  --tag-coral:            #FB7185;
  --tag-coral-bg:         rgba(251, 113, 133, 0.10);
  /* Amber (was a second pink, #F472B6 — too close to coral, so channel/tag
     colours clustered on one tone). A warm gold fills the empty slot and spreads
     the 6-colour rotation across the wheel. */
  --tag-amber:            #FBBF24;
  --tag-amber-bg:         rgba(251, 191, 36, 0.10);
  --tag-green:            #4ADE80;
  --tag-green-bg:         rgba(74, 222, 128, 0.10);
  /* Honey gold — warm palette member (not the alert orange #f59e0b). Used for
     the search Transcript glyph so it leaves the Curator tier accent (#A78BFA). */
  --tag-gold:             #F2C94C;
  --tag-gold-bg:          rgba(242, 201, 76, 0.10);

  /* ── Universal 20-pillar interest palette (dark) ─────────────────────────
     One fixed hue per master pillar = stable identity on every surface (detail,
     row, channel, trending, search facet, future graph node). Reuses 8 existing
     --tag and --accent app hues for harmony (noted inline); the other 12 are
     sympathetic siblings around the same hue wheel. Each is verified clear of the
     tier accents (Curator #A78BFA, Analyst #F472B6, Navigator #4F46E5, brand
     #0698ea). Pill bg/border derive via color-mix in the .cat-pill-* rules. */
  --pillar-ai:                   #5CE6FF;
  --pillar-travel:               #56C7F5;
  --pillar-education:            #64B5F6; /* = --tag-blue */
  --pillar-technology:           #4D8FE8;
  --pillar-business:             #8893F2;
  --pillar-psychology:           #B79CFF;
  --pillar-news-society:         #B388E8;
  --pillar-careers:              #D88FD8;
  --pillar-music:                #E879C6;
  --pillar-creative-design:      #FB8FB0;
  --pillar-entertainment:        #FB7185; /* = --tag-coral */
  --pillar-marketing:            #F59E0B; /* = --accent-orange */
  --pillar-food-drink:           #FBBF24; /* = --pill-processing-fg */
  --pillar-personal-development: #F2C94C; /* = --tag-gold */
  --pillar-sports:               #B4E157;
  --pillar-finance:              #4ADE80; /* = --tag-green */
  --pillar-health-fitness:       #34D399;
  --pillar-science:              #C77BE8;
  --pillar-productivity:         #5EEAD4; /* = --tag-teal */
  --pillar-home-diy:             #D6A06A;

  /* Motion — design language v1 §7 */
  --motion-hover:          140ms;
  --motion-accordion:      220ms;
  /* Row hover-reveal (action-item cluster + thumb play overlay) is an INSTANT
     switch, not a fade: during a fade the row text shows through the
     semi-opaque icons, and across many rows that transitional bleed reads as
     constant flicker. One-off fades are fine; a repeated grid is not. */
  --motion-action-fade:    0ms;

  /* Hover layer tokens — design language v1 §9 */
  --card-rest:    var(--surface-2);
  --card-hover:   var(--surface);
  --border-rest:  transparent;
  --border-hover: var(--border);

  --stripe-rest: var(--border);

  /* Elevation scale (2026-06-08). A small, theme-aware shadow ramp for FLOATING
     surfaces (menus, popovers, toasts, modals). Dark mode: a single deep cast on
     near-black reads as elevation. Light mode (overridden below): softer,
     lower-alpha, slightly cool-tinted, two-layer (contact + ambient) shadows —
     a heavy black cast reads as dirt on a light bg. sm → md → lg by float height. */
  --shadow-sm: 0 4px 12px rgba(0, 0, 0, 0.30);
  --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.40);
  --shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.45);
}

html[data-theme="light"]{
  color-scheme: light;

  /* Light theme = "Paper" (2026-08-13, replaces "Soft Grey" 2026-06-08). Same
     wash construction as dark, inverted: an INDIGO wash mixed into a white page,
     so surfaces step DOWN from the ground instead of up. That inversion is
     forced, not stylistic — on a white page you cannot go lighter, so elevation
     has to be expressed as darker plus shadow (see --shadow-*). Cards therefore
     sit at the lightest step and floating menus at the darkest.

     The wash is #0e1742 rather than neutral black for the same reason it is
     indigo in dark: a black wash greys the page out as it deepens, where the
     tint keeps the surfaces in the brand's cool family.

     Text is one warm near-black stepped down a four-stop ramp toward --bg at 75%
     fall-off; every stop clears AA on the card (13.44 / 9.72 / 7.15 / 3.72:1).
     Meta lands where the dark theme's does — fine for eyebrows and timestamps,
     but do not demote real body copy to --text-meta on either theme. */
  --bg: #FFFFFF;
  --surface: color-mix(in srgb, #0e1742 13.5%, var(--bg));
  --surface-2: color-mix(in srgb, #0e1742 10%, var(--bg));
  --surface-chrome: color-mix(in srgb, #0e1742 17%, var(--bg));
  --control-bg: rgba(0,0,0,0.05);

  --text: #1F1F1F;
  --text-body: #3A3633;
  --text-preview: #4F4A43;
  --text-meta: #7C756D;
  --text-disabled: #AA9F94;
  --muted: var(--text-meta);

  /* Hairline is BLACK-based here. The dark theme's rgba(255,255,255,.09) would
     be invisible on a white page — the one token that cannot simply carry over. */
  --border: rgba(0,0,0,0.09);
  --border-2: rgba(0,0,0,0.16);

  --btn: rgba(11, 132, 204, 0.06);
  --btn-hover: rgba(11, 132, 204, 0.11);

  --accent-green: #1f9a56;
  --accent-green-strong: #187a44;
  --accent-green-soft: rgba(31, 154, 86, 0.42);
  --accent-green-bg: rgba(31, 154, 86, 0.14);
  --accent-green-bg-hover: rgba(31, 154, 86, 0.2);

  --accent-orange: #de8d12;
  --accent-orange-strong: #b36f09;
  --accent-orange-soft: rgba(222, 141, 18, 0.42);

  --accent-red: #dc3d49;
  --accent-red-soft: rgba(220, 61, 73, 0.38);

  --accent-blue: #0b84cc;
  --accent-blue-soft: rgba(11, 132, 204, 0.34);
  --accent-blue-bg: rgba(11, 132, 204, 0.12);
  --accent-blue-bg-hover: rgba(11, 132, 204, 0.18);

  --callout-bg: rgba(33,150,243,0.10);
  --callout-accent: #2196F3;
  --callout-bg-processing: rgba(251,191,36,0.10);

  /* Status-pill foregrounds MUST be light-adapted too (2026-06-08): the dark
     defaults are luminous (#4ADE80 etc.) and fall to ~1.6–2.4:1 on the light
     pills. These saturated darks restore ≥4.5:1 while keeping the hue. */
  --pill-done-fg:         #15803D;
  --pill-processing-fg:   #B45309;
  --pill-failed-fg:       #B91C1C;
  --pill-queued-fg:       #475569;
  --pill-done-bg:         rgba(34, 197, 94, 0.20);
  --pill-processing-bg:   rgba(251, 191, 36, 0.20);
  --pill-failed-bg:       rgba(239, 68, 68, 0.20);
  --pill-queued-bg:       rgba(107, 125, 146, 0.20);

  --tag-blue:             #1976D2;
  --tag-blue-bg:          rgba(25, 118, 210, 0.15);
  --tag-purple:           #7C3AED;
  --tag-purple-bg:        rgba(124, 58, 237, 0.15);
  --tag-teal:             #0D9488;
  --tag-teal-bg:          rgba(13, 148, 136, 0.15);
  --tag-coral:            #BE123C;
  --tag-coral-bg:         rgba(190, 18, 60, 0.15);
  /* Amber, light-theme darkened per the accent-adaptation pillar (was pink #DB2777). */
  --tag-amber:            #B45309;
  --tag-amber-bg:         rgba(180, 83, 9, 0.15);
  --tag-green:            #16A34A;
  --tag-green-bg:         rgba(22, 163, 74, 0.15);
  /* Honey gold, light-theme darkened per the accent-adaptation pillar. */
  --tag-gold:             #B7791F;
  --tag-gold-bg:          rgba(183, 121, 31, 0.15);

  /* ── Universal 20-pillar interest palette (light) ────────────────────────
     Same hues, saturated/darkened to clear WCAG AA on the Soft Grey surfaces
     (the per-theme accent-adaptation pillar). Same fixed-identity mapping as the
     dark block; verified clear of the tier accents. */
  --pillar-ai:                   #0E7490;
  --pillar-travel:               #0284C7;
  --pillar-education:            #1976D2;
  --pillar-technology:           #1E5FBF;
  --pillar-business:             #3730A3;
  --pillar-psychology:           #6D28D9;
  --pillar-news-society:         #7E22CE;
  --pillar-careers:              #9D2C8F;
  --pillar-music:                #A21CAF;
  --pillar-creative-design:      #BE185D;
  --pillar-entertainment:        #BE123C;
  --pillar-marketing:            #C2410C;
  --pillar-food-drink:           #B45309;
  --pillar-personal-development: #B7791F;
  --pillar-sports:               #4D7C0F;
  --pillar-finance:              #16A34A;
  --pillar-health-fitness:       #047857;
  --pillar-science:              #8E2DBF;
  --pillar-productivity:         #0D9488;
  --pillar-home-diy:             #8A5320;

  /* Hover layer tokens — design language v1 §9. Soft Grey: rows/tiles rest as a
     light grey (lifts above the --bg without going stark white), hover lifts a
     touch brighter (more emphasis, not less — fixes the prior inversion where
     hover went LIGHTER than rest and dissolved into the bg). These are
     edge-accent rows (left stripe + rounded-right), so the elevation cue is fill
     contrast, not a drop shadow. */
  --card-rest:    #F1F2F4;
  --card-hover:   #F8F9FA;
  --border-rest:  transparent;
  --border-hover: var(--border);

  --stripe-rest: var(--border);

  /* Soft Grey elevation — two-layer (contact + ambient), low-alpha, faintly cool
     (rgb 20,24,31). On a light bg a shadow should read as a gentle lift, not a
     dark smear; the contact layer grounds the surface, the ambient layer gives
     depth. */
  --shadow-sm: 0 1px 2px rgba(20,24,31,0.07), 0 2px 8px rgba(20,24,31,0.08);
  --shadow-md: 0 2px 6px rgba(20,24,31,0.08), 0 10px 24px rgba(20,24,31,0.12);
  --shadow-lg: 0 8px 20px rgba(20,24,31,0.12), 0 24px 56px rgba(20,24,31,0.16);

}

/* Light-theme tier accents (2026-06-08). The dark-mode tier colours
   (curator #A78BFA, analyst #F472B6) are luminous pastels tuned for a near-black
   bg — on a light bg they fall to ~2:1 contrast and wash out as active-nav
   glyphs / tier-card accents / selected toggles. These darker variants restore
   ≥4.5:1 while keeping the same hue family (mirrors the tag-token theme-flip).
   Navigator stays indigo (already dark enough) but deepens slightly for parity.
   Higher specificity than the base .tier-* / .account-plan-row.tier-* rules, so
   source order is irrelevant. */
html[data-theme="light"] .tier-curator,
html[data-theme="light"] .account-plan-row.tier-curator{
  --tier-accent: #7C3AED;
  --tier-accent-rgb: 124, 58, 237;
}
html[data-theme="light"] .tier-analyst,
html[data-theme="light"] .account-plan-row.tier-analyst{
  --tier-accent: #DB2777;
  --tier-accent-rgb: 219, 39, 119;
}
html[data-theme="light"] .tier-navigator,
html[data-theme="light"] .account-plan-row.tier-navigator{
  --tier-accent: #4338CA;
  --tier-accent-rgb: 67, 56, 202;
}

*{ box-sizing: border-box; }

/* Kill the native mobile tap-highlight app-wide. It's a translucent grey rectangle
   the browser paints over any tapped link/button/image, and it ALWAYS ignores
   border-radius, so on touch it read as a hard-edged scrim over rounded buttons and
   images. `-webkit-tap-highlight-color` is inherited, so one rule on html covers
   everything; each element keeps its own rounded :hover/:active press feedback. */
html{ -webkit-tap-highlight-color: transparent; }

html, body{ height: 100%; }

body{
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
  /* Canonical text size fallback. Any element without a scoped font-size
     inherits this — prevents 16px browser-default leaks. */
  font-size: var(--type-body-size);
  line-height: 1.15;
  overflow-y: scroll;
}

a{ color: inherit; }

.wrap{
  /* Sticky-footer layout: the wrap is a full-viewport-tall flex column
     (header → main → footer). .main grows to fill leftover space (flex: 1),
     so on a short page (failed save, sparse summary) the footer is pushed to
     the bottom of the visible window instead of floating mid-screen with dark
     space beneath it. On a tall page .main overflows naturally and the footer
     scrolls to the end of content — no sticky positioning needed.
     100dvh (not 100vh) tracks the iOS Safari address-bar collapse/expand so
     the footer doesn't jump when the browser chrome resizes. This single rule
     covers every viewport: wide desktop, horizontal tablet, AND the
     mobile-short-content edge case (min-height applies regardless of width). */
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  padding-top: env(safe-area-inset-top);
  /* No bottom padding here: it would sit BELOW the footer (the last flex
     child) and float it a safe-area height above the screen bottom with page
     background showing through. The inset lives on .foot instead, so the
     footer's background reaches the very bottom while its content clears the
     gesture bar. */
}

.foot{
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  justify-items: center;
  /* Bottom padding carries the safe-area inset (moved off .wrap) so the
     footer background fills to the screen bottom while its content stays
     above the gesture bar. */
  padding-top: 16px;
  padding-bottom: calc(16px + env(safe-area-inset-bottom));
  padding-inline: max(var(--layout-inset-desktop), calc((100% - var(--content-max-width)) / 2 + var(--layout-inset-desktop)));
  border-top: 1px solid var(--border);
  color: var(--muted);
  background: var(--surface);
}

.foot-center{
  grid-column: 2;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
}

.foot small{
  font-size: var(--type-foot-size);
  letter-spacing: var(--type-meta-track);
  text-transform: none;
}

.foot .icon-btn{
  color: var(--muted);
}

.foot .icon-btn:hover{
  color: var(--muted);
}

.foot-brand-line{
  text-transform: uppercase !important;
}

.foot small a{
  color: var(--muted);
  text-decoration: none;
}

.foot small a:hover{
  text-decoration: underline;
}

.foot-theme-toggle{
  grid-column: 1;
  justify-self: start;
}

.foot-right{
  grid-column: 3;
  justify-self: end;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

/* ── Primary navigation shell (2026-06-08) ──────────────────────────────────
   The old sticky top header (.top) is retired. Desktop ≥561px: a fixed left
   rail. Mobile ≤560px: a fixed bottom tab-bar. Both are position:fixed, so the
   centered .main column clears them via padding on .wrap. No sticky header →
   infinite scroll is never blocked. Opaque background (solid --bg + --surface
   overlay) so scrolled content doesn't bleed through. */
:root{ --side-rail-w: 72px; --tab-bar-h: 56px; --nav-left-offset: 0px; }
/* Thumbnail focal point (smart-crop Phase 1, 2026-06-22). Short-form reels are
   9:16 with the subject in the upper-middle, so when object-fit:cover crops a
   reel frame into a shorter thumb box (3:4 rows, 16:9 hero, square mosaic) the
   default 50% centre cuts the head off the top. Bias the crop UPWARD so faces/
   subjects stay framed. Self-limiting: object-position only bites where cover
   actually crops vertically, so already-fitting graphics are untouched. Tune
   here in one place; consumed by the thumbnail block near the end of this file. */
:root{ --thumb-focal-y: 32%; }
/* Left chrome width for fixed-to-viewport overlays (detail-nav arrows) to clear
   the rail: the rail width on desktop, 0 on mobile (tab-bar is bottom). */
@media (min-width: 561px){ :root{ --nav-left-offset: var(--side-rail-w); } }

.side-rail{
  position: fixed;
  left: 0; top: 0; bottom: 0;
  width: var(--side-rail-w);
  z-index: 20;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: calc(16px + env(safe-area-inset-top)) 0 calc(16px + env(safe-area-inset-bottom));
  border-right: 1px solid var(--border);
  background: var(--surface);
}
/* The rail brand glyph (.side-rail-brand / _brand_mark.html) and the mobile
   .mobile-brand header were retired 2026-06-09 — the brand now lives in the
   unified .top-strip (rendered in .main on both breakpoints). The rail carries
   destinations only. */
.side-rail-dests{
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}
.side-rail-logout{
  margin: auto 0 0;   /* push logout to the bottom of the rail */
  display: inline-flex;
}

.tab-bar{ display: none; }

/* Content clears the fixed rail on desktop (keyed on :has(.side-rail) so the
   pre-auth pages that drop the nav stay un-padded / centred). */
@media (min-width: 561px){
  body:has(.side-rail) .wrap{ padding-left: var(--side-rail-w); }
}
@media (max-width: 560px){
  .side-rail{ display: none; }
  body:has(.side-rail) .wrap{
    padding-bottom: calc(var(--tab-bar-h) + env(safe-area-inset-bottom));
  }
  .tab-bar{
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 20;
    display: flex;
    justify-content: space-around;
    align-items: center;
    height: calc(var(--tab-bar-h) + env(safe-area-inset-bottom));
    padding-bottom: env(safe-area-inset-bottom);
    border-top: 1px solid var(--border);
    background: var(--surface);
  }
}

/* Active destination — tier-accent glyph (cascades from body.tier-{tier};
   falls back to --text on pages without a tier class). */
.nav-dest.is-active{
  color: var(--tier-accent, var(--text));
}

/* The nav Channels glyph (brand triangle) reads ~9.5% high against the
   icon-btn's circular backplate — the brand path's geometric y-centre sits
   ~9.5% above the SVG box centre (the same offset .channel-triangle--inline
   corrects for text contexts; see CLAUDE.md canon #8). The nav is a chrome
   context so it intentionally doesn't pass inline_offset, but it needs the
   identical vertical-centring nudge to sit true inside the round button.
   Transform-only with an explicit origin per the hover-state canon. Scoped to
   .nav-dest so only the nav Channels triangle is affected (the only
   channel-triangle inside a nav destination); both rail + tab-bar inherit it. */
.nav-dest .channel-triangle{
  transform: translateY(9.5%);
  transform-origin: center;
}

.brand{
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
}

.brand a{
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}

.brand-logo{
  display: block;
  height: 20px;
  width: auto;
}

.brand-tagline{
  font-size: 0.54rem;
  line-height: 1.1;
  letter-spacing: 0.03em;
  color: var(--muted);
  white-space: nowrap;
}

.brand-logo-on-light{
  display: none;
}

html[data-theme="light"] .brand-logo-on-dark{
  display: none;
}

html[data-theme="light"] .brand-logo-on-light{
  display: block;
}

.theme-toggle{
  width: 32px;
  height: 32px;
  padding: 0;
  color: var(--text);
}

/* Theme toggle — a single icon that swaps IN PLACE with the theme; the menu
   label is a static "Theme". Show the current theme: dark → moon, light → sun.
   (The svgs deliberately DROP the .icon class so .icon{display:block} can't
   override these display toggles and reveal both at once.) */
.theme-icon{ display: none; }
.theme-moon{ display: block; }           /* default theme = dark */
html[data-theme="light"] .theme-moon{ display: none; }
html[data-theme="light"] .theme-sun{ display: block; }

/* About modal (was the page footer) — brand mark + version + legal links. */
/* Two-class specificity (.modal-card.about-modal-card) so the vertical padding
   beats the base `.modal-card { padding: 20px }` shorthand, which is defined LATER
   in the file and would otherwise win the cascade on equal specificity + source
   order (it silently capped this modal's padding at 20px). */
.modal-card.about-modal-card{ position: relative; display: flex; flex-direction: column; align-items: center; text-align: center; gap: 16px; padding-top: 48px; padding-bottom: 44px; }
/* Top-right ✕ affordance (mirrors the walkthrough close), replacing the old
   "Close" text button. Theme-aware so it reads on both backdrops. */
.about-modal-close-x{
  position: absolute; top: 12px; right: 12px;
  width: 30px; height: 30px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0.5px solid var(--border); border-radius: 50%;
  background: var(--surface-2); color: var(--text-meta);
  cursor: pointer; transition: background 140ms ease, color 140ms ease;
}
.about-modal-close-x:hover{ background: var(--btn-hover); color: var(--text); }
.about-modal-close-x svg{ display: block; }
.about-modal-head{ display: flex; justify-content: center; }
.about-modal-head .brand-logo{ width: 188px; height: auto; color: var(--text); }
.about-modal-tagline{ margin: -12px 0 0; font-size: 0.95rem; }
.about-modal-meta{ display: flex; flex-direction: column; align-items: center; gap: 5px; }
.about-modal-meta small,
.about-modal-meta a{ color: var(--text-meta); text-decoration: none; }
.about-modal-meta a:hover{ text-decoration: underline; }
/* Copyright + links read as one tight muted footer cluster (proper-case brand
   name, no uppercase eyebrow); the build version sits slightly apart below. */
.about-modal-version{ margin-top: 12px; }
.about-modal-card .modal-actions{ justify-content: center; width: 100%; }

/* Error-code legend modal (opened from the About modal's "Codes" link). */
.error-codes-card{ position: relative; max-width: 460px; padding: 28px 22px 22px; text-align: left; }
.error-codes-heading{ margin: 0; font-size: 1.05rem; }
.error-codes-intro{ margin: 6px 0 14px; font-size: 0.9rem; }
.error-codes-list{ display: flex; flex-direction: column; gap: 10px; max-height: 60vh; overflow-y: auto; }
.error-codes-row{ display: flex; align-items: flex-start; gap: 10px; }
.error-codes-row .error-code-pill{ flex: 0 0 auto; margin-top: 1px; }
.error-codes-text{ display: flex; flex-direction: column; gap: 1px; font-size: 0.88rem; }
.error-codes-text .muted{ font-size: 0.84rem; }

/* (.nav / .nav-logout-form / .brand-tagline retired with the old top header —
   nav now lives in .side-rail / .tab-bar above.) */

.file-input{
  width: 100%;
  color: var(--muted);
  font-size: var(--type-body-size);
  border: 0;
  padding: 0;
  background: transparent;
  appearance: none;
  box-shadow: none;
}

.file-input::file-selector-button{
  border-radius: 999px;
  border: 1px solid var(--border);
  padding: 10px 12px;
  margin-right: 10px;
  background: var(--btn);
  color: var(--text);
  cursor: pointer;
  line-height: 1;
  min-height: 38px;
  min-width: 110px;
}

.file-input::file-selector-button:hover{
  background: var(--btn-hover);
}

.file-input::-webkit-file-upload-button{
  border-radius: 999px;
  border: 1px solid var(--border);
  padding: 10px 12px;
  margin-right: 10px;
  background: var(--btn);
  color: var(--text);
  cursor: pointer;
  line-height: 1;
  min-height: 38px;
  min-width: 110px;
}

.file-input::-webkit-file-upload-button:hover{
  background: var(--btn-hover);
}

.sr-file-input{
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.file-picker{
  align-items: center;
  gap: var(--space-3);
  margin-block: var(--space-2);
}

.file-trigger{
  border-radius: 999px;
  border: 1px solid var(--border);
  min-height: 38px;
  min-width: 180px;
  padding: 8px 14px;
  background: var(--btn);
  color: var(--muted);
  cursor: pointer;
}

.file-trigger:hover{
  background: var(--btn-hover);
}

.file-name{
  font-size: var(--type-body-size);
  line-height: 1.2;
}

.main{
  /* flex: 1 0 auto — grow to absorb leftover column height so the footer
     anchors to the bottom of the visible window on short pages; shrink-0 +
     auto basis so tall content overflows naturally to its own height. See
     the .wrap sticky-footer note. */
  flex: 1 0 auto;
  max-width: var(--content-max-width);
  margin: 0 auto;
  width: 100%;
  padding: 24px var(--layout-inset-desktop);
}

.main p,
.main li{
  font-size: var(--type-body-size);
  line-height: 1.2;
}

.header{
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
}

.title-wrap{
  max-width: 70%;
}

.item-detail-header{
  align-items: flex-start;
}

/* Prev/next edge arrows on the detail page. Fixed to the viewport, vertically
   centred, hugging the outer edge of the centred content column (clamped to a
   12px gutter on narrower desktops). Bare tier-accent chevron — no border, no
   backplate — matching the icon design language; the 44px box is just the tap
   target. Paint-only hover per the hover canon. JS unhides them only when a
   neighbour exists in the remembered list sequence. */
.detail-nav-arrow{
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  transform-origin: center;
  /* Below the nav chrome (.side-rail / .tab-bar are position:fixed z-index:20,
     each a stacking context that TRAPS its own .more-menu z-50 + submenu z-60).
     At z-20 the chevron tied that tier and won on source order (<main> paints
     after the rail), so an open More menu rendered UNDER the floating chevron.
     Sitting at 15 keeps the chevron above content (cards are z 1-10) while
     letting the rail/tab-bar popovers paint over it. Modals (z-999) are
     unaffected either way. */
  z-index: 15;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  color: var(--tier-accent);
  text-decoration: none;
  transition: background var(--motion-hover) ease;
}
/* The `hidden` attribute must win over the base display above (an author
   `display: inline-flex` otherwise overrides the UA `[hidden]{display:none}`).
   JS unhides an arrow ONLY when a neighbour exists in that direction, so at
   the first item the prev arrow stays gone, and at the last the next arrow
   does — and on deep-links with no sequence both stay gone. */
.detail-nav-arrow[hidden]{
  display: none;
}
.detail-nav-arrow svg{
  width: 22px;
  height: 22px;
}
/* The content column is shifted right by the rail (--nav-left-offset), so the
   gutter arrows track it: +offset/2 on the left, -offset/2 on the right, with
   the prev clamp floored above the rail so it never sits under it. */
.detail-nav-prev{
  left: max(calc(var(--nav-left-offset) + 12px), calc(50% + var(--nav-left-offset) / 2 - var(--content-max-width) / 2 - 84px));
}
.detail-nav-next{
  right: max(12px, calc(50% - var(--nav-left-offset) / 2 - var(--content-max-width) / 2 - 84px));
}
/* Desktop hover/active: the nav-icon grey circle (var(--btn-hover)) behind the
   tier-accent chevron — same icon design language as the header icon buttons. */
.detail-nav-arrow:hover,
.detail-nav-arrow:active{
  background: var(--btn-hover);
}
.detail-nav-arrow:focus-visible{
  outline: 2px solid var(--tier-accent);
  outline-offset: 2px;
}
/* Bold arrow treatment — opaque tier-fill circle, white chevron, soft shadow, reading as a
   SELF-CONTAINED floating control (carousel vocabulary). Applied GENEROUSLY: the bold circle
   is the default down from 1440px, so it appears well BEFORE a bare chevron would sit tight
   against the content. A bare glyph needs real edge clearance to read cleanly; a self-contained
   circle does not. Position is NOT overridden here — the arrows keep the base gutter-tracking
   position (near the card, clamped 12px off the viewport edge once the gutter closes), so at
   1440px the circle drops to the bare chevron IN PLACE with no jump. The +84px offset keeps the
   bare chevron ~60px off the card on the wide screens (>1440px) where it appears. NOTE the
   arrows track --content-max-width (the .main box); the visible card sits one
   --layout-inset-desktop further in, so the card gap is larger than the raw offset. */
@media (max-width: 1440px){
  .detail-nav-arrow{
    width: 40px;
    height: 40px;
    background: var(--tier-accent);
    color: #fff;
    border-radius: 50%;
    box-shadow: var(--shadow-sm);
  }
  /* Keep the tier fill on tap — don't let the desktop grey-circle
     hover/active rule repaint the bold control grey under the thumb. */
  .detail-nav-arrow:hover,
  .detail-nav-arrow:active{
    background: var(--tier-accent);
  }
  .detail-nav-arrow svg{
    width: 20px;
    height: 20px;
  }
}

.item-detail-page .page-title{
  font-size: var(--type-detail-size);
  font-weight: 500;
  /* "Display tight" — multi-line detail H1 (especially on mobile) reads
     tightly without losing legibility. Scoped to .item-detail-page; the
     base .page-title rule (used on the share page H1) keeps its own
     line-height for that display context. */
  line-height: 1.2;
  letter-spacing: -0.018em;
}

.title-with-thumb{
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: 12px;
  width: 100%;
}

/* Phase 3 carousel viewer — a swipeable slide strip for a multi-image IG carousel.
   Pure CSS scroll-snap (one slide per view; swipe on touch, scroll/trackpad on
   desktop); the counter below is JS-updated. Only rendered when carousel_slides has
   2+ entries, so it's inert for every other save. */
.detail-carousel{ margin: 0 0 var(--space-section-block); }
.detail-carousel-viewport{ position: relative; }
.detail-carousel-track{
  display: flex;
  gap: 8px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  border-radius: 12px;
  scrollbar-width: thin;
}
.detail-carousel-slide{
  flex: 0 0 100%;
  scroll-snap-align: center;
  width: 100%;
  max-height: 70vh;
  object-fit: contain;
  border-radius: 12px;
  background: var(--surface-2);
}
.detail-carousel-count{
  margin-top: 6px;
  text-align: center;
  font-size: var(--type-meta-size);
  color: var(--text-meta);
  font-variant-numeric: tabular-nums;
}
/* Tap-to-advance hint — tier-coloured glyph on a soft scrim, bottom-right of the
   slide. Teaches the gesture then fades out (dismissed on first tap/swipe). */
.detail-carousel-hint{
  position: absolute;
  right: 10px;
  bottom: 10px;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  color: var(--tier-accent, #0698ea);
  background: color-mix(in srgb, var(--bg) 60%, transparent);
  box-shadow: 0 1px 6px rgba(0, 0, 0, .35);
  pointer-events: none;
  transition: opacity .45s ease;
  animation: detail-carousel-hint-nudge 1.5s ease-in-out 3;
}
.detail-carousel-hint-glyph{ width: 20px; height: 20px; }
.detail-carousel-hint.is-dismissed{ opacity: 0; }
@keyframes detail-carousel-hint-nudge{
  0%, 100% { transform: translateX(0); }
  50% { transform: translateX(-5px); }
}
@media (prefers-reduced-motion: reduce){
  .detail-carousel-hint{ animation: none; }
}
.detail-carousel-tools{ margin-top: 8px; text-align: center; }
.detail-carousel-reorder-btn{
  background: none;
  border: none;
  color: var(--text-meta);
  font-size: var(--type-meta-size);
  cursor: pointer;
  padding: 4px 6px;
}
.detail-carousel-reorder-btn:hover{ color: var(--text); }
/* Reorder panel */
.carousel-reorder{
  margin: 0 0 var(--space-section-block);
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface-2);
}
.carousel-reorder-head{
  font-size: var(--type-meta-size);
  color: var(--text-preview);
  margin-bottom: 10px;
  text-align: center;
}
.carousel-reorder-list{ list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.carousel-reorder-item{
  position: relative;
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface);
}
.carousel-reorder-item.is-dragging{
  border-color: var(--tier-accent, #0698ea);
  box-shadow: 0 10px 24px rgba(0, 0, 0, .5);
  z-index: 20;
  will-change: transform;
}
.carousel-reorder-thumb{
  display: block;
  width: 100%;
  height: 180px;
  object-fit: contain;
  background: var(--surface-2);
}
.carousel-reorder-foot{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 10px;
  border-top: 1px solid var(--border);
  color: var(--text-meta);
  cursor: grab;
  /* The GLYPH is 13px (see _channel_list.html), not the 18px it shipped at. A
     per-row drag handle rendering 1.5x the primary view toggle (12px) inverted
     the hierarchy, and being the heaviest dot-matrix on the page it pulled the
     eye away from the control that actually changes the view. The 34px button
     below is the touch target, so shrinking the glyph costs no grabbability. */
  touch-action: none;   /* drag from the footer bar without scrolling the page */
}
.carousel-reorder-foot:active{ cursor: grabbing; }
.carousel-reorder-pos{
  font-variant-numeric: tabular-nums;
  font-size: var(--type-meta-size);
  color: var(--text);
}
.carousel-reorder-grip{ opacity: .65; }
.carousel-reorder-actions{
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 12px;
}
.carousel-reorder-save{
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 8px;
  padding: 8px 14px;
  cursor: pointer;
}
.carousel-reorder-save:hover{ border-color: var(--tier-accent, #0698ea); }
/* Desktop: hug the slide width so there's no side letterbox, and keep the viewer to
   about half the column so it doesn't dominate the page. Mobile stays full-width
   (a phone should show slides big). The count + reorder tools already centre. */
@media (min-width: 561px){
  .detail-carousel-viewport{ max-width: 460px; margin-inline: auto; }
  .carousel-reorder{ max-width: 520px; margin-inline: auto; }
}
/* While reordering, the card prev/next chevrons would overlap the drag area — hide them. */
html.reordering .detail-nav-arrow{ display: none; }

.title-with-thumb .title-wrap{
  flex: 1 1 auto;
  min-width: 0;
  max-width: 100%;
}

.status-actions-row{
  align-items: center;
  justify-content: flex-start;
  gap: 12px;
  flex-wrap: nowrap;
}

.status-actions-row .status-line{
  margin: 0;
  width: auto;
}

/* Outer gap separates the two action groups (artefact vs lifecycle).
   Inner gap is tighter for icons inside one group.
   Both selectors compound .row with the action class to beat the base
   .row { gap: 12px } rule (declared later in the stylesheet at ~line 591).
   Without the compound, .row's gap wins on declaration order and the
   differential between groups collapses. */
.row.item-actions{
  margin-top: 0;
  margin-bottom: 0;
  width: auto;
  /* Uniform 6px gap across all action icons — the inter-group gap matches the
     intra-group gap so share→reprocess and reprocess→trash read evenly spaced
     (the prior 12px inter-group differential read as an uneven gap). */
  gap: 6px;
  flex-wrap: nowrap;
}

.row.item-actions-group{
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 6px;
}

/* Detail-page overflow (⋯) menu — holds download / share / reprocess so the
   action row stays uncluttered (top-level: channel-add + ⋯ + delete). */
.more-icon{
  display: block;
  /* Slightly smaller than 20px so the 3-dot glyph balances against the
     sibling row icons (play / trash) within the shared icon-btn box. */
  width: 16px;
  height: auto;
}
.more-menu-wrap{
  position: relative;
  display: inline-flex;
}
.more-menu{
  /* Fixed + JS-positioned (the shared controller in base.html sets top/left
     from the toggle's rect, right-aligned, flipping up near the viewport
     bottom). Fixed — not absolute — so the menu escapes any clipping/scroll
     ancestor on row surfaces (Library / Recents / channel-detail rows). */
  position: fixed;
  top: 0;
  left: 0;
  z-index: 50;
  min-width: 184px;
  padding: 6px;
  background: var(--surface-chrome);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-md);
  display: flex;
  flex-direction: column;
  gap: 2px;
}
/* `hidden` must win over the display:flex above (author rule beats UA). */
.more-menu[hidden]{
  display: none;
}
.more-menu-item{
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  margin: 0;
  padding: 8px 10px;
  border: none;
  background: transparent;
  color: var(--text);
  font: inherit;
  text-align: left;
  white-space: nowrap;
  border-radius: 6px;
  cursor: pointer;
  text-decoration: none;   /* the <a> menu items (My Settings / Admin) would underline */
  transition: background var(--motion-hover) ease, color var(--motion-hover) ease;
}
/* Tier-themed hover — a tier-accent tint behind the row + the icon shifting to
   the full tier accent, so the menu carries the tier identity without the
   heaviness (and white-text legibility cost) of a full tier fill. */
.more-menu-item:hover{
  background: color-mix(in srgb, var(--tier-accent) 16%, transparent);
}
.more-menu-item svg,
.more-menu-item .ti{
  width: 16px;
  height: 16px;
  font-size: 16px;
  flex-shrink: 0;
  color: var(--text-meta);
  transition: color var(--motion-hover) ease;
}
/* The video-camera glyph is ~1.6× wider than tall, so at the other icons' height it
   widens this row's icon column and knocks its label out of the shared left edge.
   Constrain its WIDTH to the 16px column instead (height follows aspect, ~10px) so
   the footprint matches the square icons and every label lines up. */
.more-menu-item .quickstart-menu-glyph{ width: 16px; height: auto; }
.more-menu-item:hover svg,
.more-menu-item:hover .ti{
  color: var(--tier-accent);
}
/* Destructive variant (channel Trash) — neutral at rest like every other menu
   row; the red only asserts on hover, matching the .icon-danger trash pattern
   (the destructive intent reveals on intent-to-click, not at rest). */
.more-menu-item-danger:hover{
  background: color-mix(in srgb, var(--pill-failed-fg) 14%, transparent);
  color: var(--pill-failed-fg);
}
.more-menu-item-danger:hover svg,
.more-menu-item-danger:hover .ti{
  color: var(--pill-failed-fg);
}

/* Channel action-sheet picker — colour-matched pill + brand triangle toggle.
   The native checkbox is visually hidden; the triangle is outline at rest and
   fills (in the channel colour) when the row is selected. Reuses the channel
   visual-identity vocabulary (--topic-fill drives both pill and triangle). */
.channel-pick{
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 4px;
  border-radius: 8px;
  cursor: pointer;
  color: var(--topic-fill);
  transition: background var(--motion-hover) ease;
}
.channel-pick:hover{
  background: color-mix(in srgb, var(--topic-fill) 8%, transparent);
}
.channel-pick-input{
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.channel-pick-tri{
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
}
/* Outline at rest (the solid-variant triangle's stroke shows; CSS fill:none
   overrides the macro's fill attribute), full fill when the row is checked. */
.channel-pick-tri svg{
  fill: none;
  transition: fill var(--motion-hover) ease;
}
.channel-pick-input:checked ~ .channel-pick-tri svg{
  fill: currentColor;
}
/* Keyboard focus ring on the (hidden) input surfaces on the pill. */
.channel-pick-input:focus-visible ~ .channel-pick-pill{
  outline: 2px solid var(--topic-fill);
  outline-offset: 2px;
}
/* Inline "+ New" create affordance — sits in line with the channel rows. The
   glyph occupies the SAME column as the channel triangles: a "+" by default,
   swapping to a FILLED triangle once a name is typed. The pill is a SOLID
   neutral outline (--topic-fill set to a muted colour) and IS the name input. */
/* Picker wraps the channel list + the "+ New" row with ONE gap, equal to the
   single-column list's 4px row gap (.channel-action-sheet-list, defined later),
   so the last-channel→new-row gap matches the channel spacing. */
.channel-picker{
  display: flex;
  flex-direction: column;
  gap: 4px;
}
/* Channel picker columns: once there are >5 channels they flow top-to-bottom
   in columns of 5 (5 row tracks, column auto-flow) — 2 cols at 6–10, 3 at
   11–15, etc. Columns size to content with a floor; horizontal overflow scrolls
   as a safety for very many. Ultra-narrow viewports collapse to one column.
   Selector is .channel-action-sheet-list.channel-list--cols (specificity 0,2,0)
   so it beats the base .channel-action-sheet-list flex rule defined LATER in
   this file — otherwise that later rule's display:flex would win and force a
   single column. */
.channel-action-sheet-list.channel-list--cols{
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: repeat(5, auto);
  grid-auto-columns: minmax(140px, max-content);
  gap: 4px 16px;
  overflow-x: auto;
}
@media (max-width: 560px){
  .channel-action-sheet-list.channel-list--cols{
    display: flex;
    flex-direction: column;
    overflow-x: visible;
  }
}

.channel-new{
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 6px;
  border-radius: 8px;
  /* Neutral solid outline for the new pill (no channel colour yet). */
  --topic-fill: var(--text-meta);
  color: var(--text-meta);
  transition: color var(--motion-hover) ease, background var(--motion-hover) ease;
}
/* Hover / once a name is typed → brand blue (matching the CTA button), so the
   new-pill reads as the active "create" affordance: the pill outline
   (--topic-fill), the "+" glyph + filled triangle (color), AND a blue tint
   behind the whole row. Deliberately NOT :focus-within — the create-only modal
   auto-focuses this field on open, and the blue should appear on intent (hover)
   or content (a typed name), not merely because the caret landed here. */
.channel-new:hover,
.channel-new.has-value{
  --topic-fill: var(--callout-accent);
  color: var(--callout-accent);
  background: color-mix(in srgb, var(--callout-accent) 10%, transparent);
}
.channel-new-glyph{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 12px;
}
/* "+" glyph (brand plus SVG) — currentColor so it's neutral at rest and brand
   blue on hover (.channel-new:hover sets color). Hidden when a name is typed
   (the filled triangle takes over). */
.channel-new-plus{
  display: inline-flex;
  align-items: center;
  color: var(--text-meta);
}
.channel-new-plus svg{
  width: 9px;
  height: 9px;
}
.channel-new-tri{
  display: none;
  align-items: center;
}
.channel-new-tri svg{
  fill: currentColor;  /* filled — only shown once a name is typed */
}
.channel-new.has-value .channel-new-plus{
  display: none;
}
.channel-new.has-value .channel-new-tri{
  display: inline-flex;
}
.channel-new-pill{
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  min-height: 22px;
}
.channel-new-input{
  flex: 1 1 auto;
  min-width: 0;
  background: transparent;
  border: none;
  color: var(--text);
  font: inherit;
  font-size: var(--type-meta-size);
  /* Left padding pushes the caret + text past the pill's rounded corner (the
     999px radius curves ~11px in; the pill's own 8px padding alone left the
     caret inside the curve, so it rendered clipped). */
  padding: 0 0 0 6px;
  outline: none;
}
.channel-new-input::placeholder{
  color: var(--text-meta);
}
.more-menu-item span{
  flex: 1 1 auto;
}

.page-title{
  margin: 0;
  max-width: 100%;
  overflow-wrap: anywhere;
  font-size: var(--type-display-size);
  line-height: var(--type-display-line);
  letter-spacing: var(--type-display-track);
  /* Canon: only two weights across the product (400 regular, 500 bold).
     Was 700 — corrected. The detail-page scoped override at
     .item-detail-page .page-title now redundantly restates this; kept
     for self-documentation of the detail-page-specific size override. */
  font-weight: 500;
}

.icon-title{
  display: inline-flex;
  align-items: center;
}

.icon-lg{
  width: 26px;
  height: 26px;
}

.sr-only{
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  border: 0;
}

.section-label{
  font-size: var(--type-label-size);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: var(--type-meta-track);
  color: var(--text-meta);
}

.stack{
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.row{
  display: flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
}

.card{
  border: 0.5px solid var(--border);
  border-radius: 12px;
  padding: 16px;
  background: var(--surface);
}

/* Full-flatten surfaces (2026-06-07, extended 2026-06-08). The dashboard,
   Library, Channel index, and account surfaces drop their rounded card chrome
   so the heading + items span the content column edge-to-edge with no container
   padding — a calmer, gallery-like read. These work BECAUSE their inner items
   (rows, tiles, bars) carry their own backgrounds, so the outer container was
   redundant. Scoped to these surface classes ONLY so login/auth (.auth-panel),
   legal, modals, and the bespoke cards below keep their frames.

   Deliberate exclusion:
   - .account-card.checkout-card-highlight — the highlighted checkout CTA keeps
     its framed/green-tinted card so it still stands out.

   NOT here (handled by the outline-only rule below): admin operator pages + the
   save detail / public share pages — their cards ARE the content (no
   self-styled inner items), so a full flatten reads stark; they keep the
   outline + padding and only drop the fill. */
.card.library-shell,
.card.dashboard-card,
.account-card:not(.checkout-card-highlight),
.item-detail-page .summary-card{
  border: 0;
  border-radius: 0;
  padding: 0;
  background: transparent;
  overflow: visible;
}
/* Outline-only content-block surfaces: admin operator pages (economics + plan
   limits) and the save detail / public share pages. Their cards group genuine
   content (dense metrics; the Summary / Source synthesis blocks), so keep the
   section outline + padding (border/radius/padding inherit from base .card) and
   drop only the background fill. The detail processing card is excluded — it
   keeps its amber/grey accent stripe (a state cue, not chrome). */
.admin-econ-root > .card,
.admin-econ-root > form > .card,
.item-detail-page .card:not(.processing-card){
  background: transparent;
}
/* The section-hover tint coloured the (now-removed) card border — neutralise. */
.card.dashboard-section:hover{
  border-color: transparent;
}


.card > strong,
.card-heading,
.summary-title strong{
  font-size: var(--type-card-title-size);
  line-height: var(--type-card-title-line);
  letter-spacing: var(--type-card-title-track);
  text-transform: uppercase;
  font-weight: 400;
}

details.card{
  padding: 16px;
}

details.card > *:not(summary){
  padding: 0;
  margin-top: 12px;
}

/* Processing card — asymmetric accent edge with state-driven stripe colour.
   Mirrors the Instagram connection-row traffic-light pattern on /ui/items/new
   and the admin-action-row pattern: the stripe IS the state signal.
   - Queued    → slate stripe (--pill-queued-fg) + 6% slate tint
   - Processing → amber stripe (--pill-processing-fg) + 6% amber tint
   - Failed    → red stripe (--pill-failed-fg) + 6% red tint
   Done state hides the card (handled in template — no class for done). */
/* Compound .card.processing-card (0,2,0) so the asymmetric accent-edge
   geometry — square LEFT (where the 3px stripe sits), rounded right — beats
   the base .card { border-radius } AND the mobile @media .card override
   (both 0,1,0). Without this, at ≤560px the .card rule re-rounds the left and
   the accent stripe gains an unwanted LHS radius. */
.card.processing-card{
  position: relative;
  border: 0;
  border-left: 3px solid var(--pill-queued-fg);
  border-radius: 0 10px 10px 0;
  padding: 10px 14px;
  background: rgba(168, 181, 199, 0.06);
  margin-bottom: 12px;
  transition: background var(--motion-hover) ease, border-left-color var(--motion-hover) ease;
}

.card.processing-card::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
}

.processing-card.processing-card--processing{
  border-left-color: var(--pill-processing-fg);
  background: rgba(251, 191, 36, 0.06);
}

/* The progress-bar card is now the single home for the descriptive status
   line — the action row above carries only the rotating spinner. Tint the
   message amber to match the stripe so the in-flight state reads as one
   cohesive amber unit (stripe + message), not a neutral caption. */
.processing-card.processing-card--processing .muted{
  color: var(--pill-processing-fg);
}

.processing-card.processing-card--failed{
  border-left-color: var(--pill-failed-fg);
  background: rgba(248, 113, 113, 0.06);
}

/* Sub-card headings that sit INSIDE a bordered card — the processing state and the
   error/degraded cards (source-unavailable / failed / RN-103 photo-post). These match the
   KEY POINTS / SOURCE section-label size, NOT the big SUMMARY card title (which is a
   flattened-surface heading). Overrides the .card > strong card-title treatment by source
   order. */
.processing-card > strong,
.source-unavailable-card > strong,
.failed-error-card > strong{
  font-size: var(--type-meta-size);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-meta);
}
/* The error/degraded card is the primary card on a failed item (no Summary card), so its
   heading takes the brighter sub-header colour (--text, like KEY POINTS / PERFORMANCE)
   rather than the muted SOURCE/processing grey. */
.source-unavailable-card > strong,
.failed-error-card > strong{
  color: var(--text);
}

.processing-card .muted{
  font-size: var(--type-body-size);
  line-height: 1.2;
  letter-spacing: 0;
}

.plan-card .muted{
  font-size: var(--type-body-size);
  font-weight: 400;
  line-height: 1.2;
}

/* Over-limit / locked-notice row — amber stripe-bearing row (action-needed
   semantic, distinct from done-green and failed-red). Matches the
   trial-expired traffic-light pattern from /ui/items/new. */
.plan-status-note{
  position: relative;
  border: 0;
  border-left: 3px solid var(--pill-processing-fg);
  border-radius: 0 10px 10px 0;
  padding: 10px 14px;
  background: rgba(251, 191, 36, 0.06);
  gap: 4px;
}

.plan-status-note::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
}

.plan-status-note > .muted{
  color: var(--pill-processing-fg);
  font-size: var(--type-body-size);
  font-weight: 400;
}

.plan-status-note a{
  color: var(--text-meta);
  text-decoration: none;
  transition: color var(--motion-hover) ease;
}

.plan-status-note a:hover{
  color: var(--pill-processing-fg);
}

.plan-card .dashboard-mini-track{
  margin-top: 2px;
  margin-bottom: 6px;
}

/* Tier-tinted plan-card backgrounds deprecated. The plan-card uses the standard
   neutral .card vocabulary (var(--surface), 0.5px border, 10px radius). The
   pink-tinted .plan-card-usage-analyst variant is the legacy pre-design-language
   treatment; selectors retained as no-ops in case template strings still emit
   them, but no visual treatment. Tier identity surfaces on the tier card, not
   here. */
.plan-card-usage-curator,
.plan-card-usage-analyst,
.plan-card-usage-navigator{
  /* No tier-coloured tinting. Standard .card vocabulary inherited. */
}

.plan-card-head{
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.progress{
  height: 8px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--border);
  overflow: hidden;
}

.progress-fill{
  height: 100%;
  background: linear-gradient(90deg, var(--accent-orange), var(--accent-green));
  border-radius: inherit;
}

.summary-row{
  list-style: none;
  cursor: pointer;
  padding: var(--space-6);
  padding-inline-start: var(--space-6);
  margin: 0;
  text-indent: 0;
  display: block;
}

.summary-row::after{
  content: none;
  display: none;
}

.summary-title{
  display: inline-flex;
  gap: 6px;
  align-items: center;
  margin-inline-start: 0 !important;
  padding-inline-start: 0 !important;
}

.summary-head{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  width: 100%;
}

.accordion-toggle{
  width: var(--icon-btn-size);
  height: var(--icon-btn-size);
  min-width: var(--icon-btn-size);
  min-height: var(--icon-btn-size);
  color: var(--muted);
  pointer-events: none;
  border-radius: 50%;
  transition: background-color 120ms ease, color 120ms ease;
}

.summary-row:hover .accordion-toggle{
  background: var(--btn-hover);
}

.accordion-toggle .icon-minus{
  display: none;
}

details[open] > summary .accordion-toggle .icon-plus{
  display: none;
}

details[open] > summary .accordion-toggle .icon-minus{
  display: block;
}

.summary-status{
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  font-size: 11px;
  color: var(--muted);
  margin-top: var(--space-4);
  width: 100%;
  justify-content: flex-start;
  margin-inline-start: 0 !important;
  padding-inline-start: 0 !important;
}

.summary-chip{
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 2px var(--space-3) 2px 22px;
  min-height: 22px;
  border-radius: 999px;
  border: 1px solid var(--border);
  font-size: var(--type-meta-size);
  font-weight: 400;
  line-height: 1.1;
  position: relative;
  white-space: nowrap;
}

.extracted-content > summary.summary-row{
  padding: 0;
  padding-inline-start: 0;
}

.extracted-content > summary .summary-title,
.extracted-content > summary .summary-status{
  margin-left: 0;
  padding-left: 0;
}

.summary-chip::before{
  content: "✓";
  position: absolute;
  left: 8px;
  font-weight: 700;
  font-size: var(--type-meta-size);
  opacity: 0.2;
}

.summary-chip.chip-ready{
  border-color: var(--accent-blue-soft);
  color: var(--accent-blue);
}

.summary-chip.chip-ready::before{
  opacity: 1;
}

.summary-chip.chip-processing{
  border-color: var(--accent-orange-soft);
  color: var(--accent-orange);
}

.summary-chip.chip-processing::before{
  content: "●";
  opacity: 1;
}

.summary-chip.chip-off{
  opacity: 0.6;
}

.subtitle.chip-ready{
  color: var(--accent-blue);
}

.subtitle.chip-processing{
  color: var(--accent-orange);
}

.subtitle.chip-off{
  color: var(--muted);
}

/* Section labels (Summary card subsections like KEY POINTS, CORE TAKEAWAY).
   Matches the canonical section-label treatment used by SOURCE / CONTENT /
   SUMMARY card-headings, but smaller (11px) — these are subsections inside
   a card, not the card's own heading. */
.section-title{
  font-size: var(--type-meta-size);
  line-height: 1.15;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text);
}

/* Performance section reuses the Key Points `.bullet-list` treatment verbatim
   (same body size / colour / weight), so it needs no bespoke styling. The
   "Top comments" sub-list uses the existing `.subtitle` content label. */

/* Top-comment @handle: reads as body text at rest (overriding the global link
   blue), opens the commenter's IG profile, and hovers to the viewer's tier
   colour. Paint-only hover (colour) per the hover canon. */
.comment-handle{
  color: inherit;
  text-decoration: none;
  transition: color 120ms ease;
}
.comment-handle:hover,
.comment-handle:focus-visible{
  color: var(--tier-accent);
}

/* Content-area subtitles (Caption / Transcription / Extracted Text / Image
   Description / Hashtags / Sites). One step below body, sentence case —
   reads as a content label, not a section label. */
.subtitle{
  font-size: var(--type-preview-size);
  line-height: 1.15;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text);
}

/* In expanded Content accordion, match Summary hierarchy:
   white section headings, muted body copy. */
.extracted-content .subtitle{
  color: var(--text);
}

.extracted-content p,
.extracted-content li{
  color: var(--muted);
}

.extracted-content p{
  margin: 0 0 10px;
}

.extracted-content p:last-of-type{
  margin-bottom: 0;
}

.extracted-content .subtitle{
  margin: 0 0 8px;
}

.extracted-content .section-divider{
  border: 0;
  border-top: 1px solid var(--border);
  opacity: 0.35;
  margin: 10px 0;
}

.icon-danger{
  color: var(--text-meta);
  transition: color var(--motion-hover) ease, background var(--motion-hover) ease;
}

.icon-danger:hover{
  color: var(--pill-failed-fg);
  border-color: var(--accent-red-soft);
}

/* Trash + share are brand-supplied OUTLINE (stroke) icons. No fill — the SVG
   carries fill="none" stroke="currentColor", and the colour inherits from the
   surrounding icon-button's `color` (icon-muted / icon-danger). A `fill` here
   would override the SVG's fill="none" and flood the outline solid. */
.trash-icon,
.share-icon{
  /* 14px box so the brand outline glyphs sit a touch smaller and match the
     visual weight of the Tabler font-icon sibling (reprocess at 16px — Tabler
     glyphs carry less mass, so ~2px larger reads as equal weight). */
  width: 14px;
  height: 14px;
  display: block;
}

/* Download is a SOLID glyph (vs the outline share/trash siblings). Solids
   carry more optical mass, so 13px reads as equal weight to the 14px outlines
   beside it (same compensation as the solid pen rename glyph). */
.download-icon{
  width: 13px;
  height: 13px;
  display: block;
}

/* Channel-detail rename affordance glyph (the solid pen). 13px — small and
   light, since it's a solid filled shape (no stroke to thin) and a secondary
   inline-with-heading affordance beside the channel name. */
.channel-detail-edit-icon{
  width: 13px;
  height: 13px;
  display: block;
  fill: currentColor;
}

summary.status-processing::after,
details.accordion-processing > summary.summary-row::after{
  color: var(--accent-orange);
}

summary.status-done::after,
details.accordion-ready > summary.summary-row::after{
  color: var(--accent-green);
}

summary.status-failed::after,
details.accordion-failed > summary.summary-row::after{
  color: var(--accent-red);
}

summary::-webkit-details-marker{
  display: none;
}

summary{
  list-style: none;
  display: block;
  padding-inline-start: 0;
}

summary::marker{
  content: none;
  display: none;
}

details.card > summary.summary-row{
  margin: 0;
  padding: 0;
  padding-inline-start: 0;
  list-style: none;
}

details.card > summary.summary-row::marker{
  content: none;
  display: none;
}

/* Hard reset: some mobile/webview engines keep summary marker gutter
   unless both element and pseudo styles are forced on details > summary. */
details > summary{
  display: block !important;
  list-style: none !important;
  padding-inline-start: 0 !important;
  margin-inline-start: 0 !important;
}

details > summary::marker{
  content: "" !important;
  display: none !important;
}

details > summary::-webkit-details-marker{
  display: none !important;
}

.ig-status{
  gap: 6px;
  border-color: var(--border-2);
}

.new-item-page label{
  display: block;
}

.new-item-page label .card-heading{
  display: block;
  margin-bottom: var(--space-3);
}

.helper-copy{
  font-size: var(--type-body-size);
  line-height: 1.2;
}
/* "connected cookies" opens the guided connect wizard (the red button stays the
   quick file-picker for users who already know the drill). Tier-coloured inline
   link within the muted helper copy. */
.new-item-cookies-link{ color: var(--tier-accent, #0698ea); text-decoration: none; transition: color 140ms ease; }
.new-item-cookies-link:hover{ color: var(--text); }

.ig-ok{
  border-color: var(--accent-blue-soft);
}

.ig-bad{
  border-color: rgba(255, 90, 90, 0.35);
}

.form-error{
  border-color: rgba(239, 68, 68, 0.45);
}

/* Channel create/rename validation notice (partials/_channel_error_banner.html) —
   surfaces the ?error=... redirect so a rejected name is no longer a silent reload. */
.channel-error-banner{
  margin-bottom: var(--space-4, 10px);
  padding: 10px 12px;
  border: 0.5px solid rgba(239, 68, 68, 0.45);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text);
  font-size: var(--type-meta-size);
}

.ig-toggle{
  display: inline-flex;
  align-items: center;
  gap: 8px;
  border-radius: 999px;
  border: 1px solid var(--border);
  padding: 8px 14px;
  font-size: 13px;
  background: rgba(255, 255, 255, 0.02);
  min-height: 38px;
  min-width: 180px;
  justify-content: center;
}

.ig-toggle-full{
  width: 100%;
  min-width: 0;
}

.ig-toggle[disabled]{
  opacity: 0.6;
  cursor: not-allowed;
}

.ig-on{
  color: var(--accent-blue);
  border-color: var(--accent-blue-soft);
}

.ig-off{
  color: var(--muted);
  border-color: var(--border);
}

.status-line{
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  font-size: var(--type-meta-size);
  letter-spacing: var(--type-meta-track);
  text-transform: uppercase;
}

.status-pill{
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 3px 9px;
  min-height: 22px;
  border-radius: 10px;
  border: 0;
  font-size: var(--type-meta-size);
  font-weight: 400;
  line-height: 1.1;
  text-transform: capitalize;
}

/* Queued pill's brand history/clock SVG — sized to the pill text cap-height so
   it sits like the sibling Tabler glyphs on the processing/failed pills. */
.status-pill .status-pill-icon{
  width: 1em;
  height: 1em;
  flex-shrink: 0;
  display: block;
}

.tag{
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 2px var(--space-3);
  min-height: 22px;
  border-radius: 999px;
  border: 1px solid var(--border);
  font-size: var(--type-meta-size);
  font-weight: 400;
  line-height: 1.1;
  margin-left: 0;
  text-transform: none;
  text-decoration: none;
}

.signals-block{
  padding-top: 4px;
  padding-bottom: 2px;
}

.signals-pills{
  gap: var(--space-3);
  margin-top: var(--space-3);
}

.signals-pills .tag{
  margin-left: 0;
  padding: 2px var(--space-3);
}

.signals-upgrade-pill{
  pointer-events: auto;
  cursor: default;
  position: relative;
}

.signals-upgrade-pill::after{
  content: "";
  position: absolute;
  left: 50%;
  bottom: calc(100% + 6px);
  transform: translateX(-50%);
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--accent-blue);
  font-size: 11px;
  line-height: 1;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease;
  z-index: 10;
}

.signals-upgrade-pill:hover::after{
  content: "Upgrade";
  opacity: 1;
}

.share-page .signals-block{
  padding-top: 0;
  padding-bottom: 0;
}

.share-page .signals-pills{
  margin-top: 0;
  gap: 6px;
  row-gap: 6px;
  column-gap: 6px;
}

.share-page .share-content-card{
  gap: 8px !important;
}

.share-page .share-content-head{
  gap: 6px;
  row-gap: 6px;
  column-gap: 6px;
  margin-top: 2px;
}

.share-page .share-content-pills{
  margin-top: 0;
  gap: 6px;
  row-gap: 6px;
  column-gap: 6px;
}

.tag-ok{
  border-color: var(--accent-blue-soft);
  color: var(--accent-blue);
}

.tag-muted{
  color: var(--muted);
}

.tag-disabled{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  color: var(--muted);
  border-color: var(--border);
  background: var(--surface-2);
}

.tag-disabled:hover{
  text-decoration: none;
  color: var(--text);
  border-color: var(--border-2);
  background: var(--btn-hover);
}

.tag-disabled-static{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  color: var(--muted);
  border-color: var(--border);
  background: var(--surface-2);
  pointer-events: none;
  cursor: default;
}

.tag-disabled-static:hover{
  text-decoration: none;
  color: var(--muted);
  border-color: var(--border);
  background: var(--surface-2);
}

.tag-disabled-static.signals-upgrade-pill{
  pointer-events: auto;
}

.tag-link{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  color: var(--accent-blue);
  border-color: var(--accent-blue-soft);
}

.tag-link:hover{
  text-decoration: underline;
}


.tag-plan-analyst{
  border-color: rgba(244, 114, 182, 0.56);
  color: #f472b6;
  background: rgba(244, 114, 182, 0.12);
}


.tag-plan-curator{
  border-color: rgba(167, 139, 250, 0.56);
  color: #a78bfa;
  background: rgba(167, 139, 250, 0.12);
}

.tag-plan-navigator{
  border-color: rgba(99, 102, 241, 0.62);
  color: #818cf8;
  background: rgba(79, 70, 229, 0.16);
}

.tag-econ-amber{
  border-color: rgba(245, 158, 11, 0.56);
  color: #f59e0b;
  background: rgba(245, 158, 11, 0.12);
}

.tag-econ-mint{
  border-color: rgba(52, 211, 153, 0.56);
  color: #34d399;
  background: rgba(52, 211, 153, 0.12);
}

.tag-econ-violet{
  border-color: rgba(167, 139, 250, 0.56);
  color: #a78bfa;
  background: rgba(167, 139, 250, 0.12);
}

.tag-econ-pink{
  border-color: rgba(244, 114, 182, 0.56);
  color: #f472b6;
  background: rgba(244, 114, 182, 0.12);
}

.tag-pill{
  border-radius: 999px;
  border-width: 0.5px;
}
.tag-pill-blue{
  color: var(--tag-blue);
  background: var(--tag-blue-bg);
  border-color: rgba(100, 181, 246, 0.30);
}
.tag-pill-purple{
  color: var(--tag-purple);
  background: var(--tag-purple-bg);
  border-color: rgba(167, 139, 250, 0.30);
}
.tag-pill-teal{
  color: var(--tag-teal);
  background: var(--tag-teal-bg);
  border-color: rgba(94, 234, 212, 0.30);
}
.tag-pill-coral{
  color: var(--tag-coral);
  background: var(--tag-coral-bg);
  border-color: rgba(251, 113, 133, 0.30);
}
.tag-pill-amber{
  color: var(--tag-amber);
  background: var(--tag-amber-bg);
  border-color: rgba(251, 191, 36, 0.30);
}
.tag-pill-green{
  color: var(--tag-green);
  background: var(--tag-green-bg);
  border-color: rgba(74, 222, 128, 0.30);
}

/* Category pills (Domain · Category · Specific) — six-colour hash-derived
   rotation, sharing the .tag-pill colour tokens but rendered at 12px radius
   (rectangular-rounded, not capsule). Used inside the SUMMARY card to
   surface the item's taxonomy. */
.cat-pill{
  display: inline-flex;
  align-items: center;
  padding: 2px 9px;
  min-height: 22px;
  border-radius: 12px;
  border: 0.5px solid var(--border);
  font-size: var(--type-meta-size);
  font-weight: 400;
  line-height: 1.1;
  white-space: nowrap;
  text-decoration: none;
}
.cat-pill-blue{
  color: var(--tag-blue);
  background: var(--tag-blue-bg);
  border-color: rgba(100, 181, 246, 0.30);
}
.cat-pill-purple{
  color: var(--tag-purple);
  background: var(--tag-purple-bg);
  border-color: rgba(167, 139, 250, 0.30);
}
.cat-pill-teal{
  color: var(--tag-teal);
  background: var(--tag-teal-bg);
  border-color: rgba(94, 234, 212, 0.30);
}
.cat-pill-coral{
  color: var(--tag-coral);
  background: var(--tag-coral-bg);
  border-color: rgba(251, 113, 133, 0.30);
}
.cat-pill-amber{
  color: var(--tag-amber);
  background: var(--tag-amber-bg);
  border-color: rgba(251, 191, 36, 0.30);
}
.cat-pill-green{
  color: var(--tag-green);
  background: var(--tag-green-bg);
  border-color: rgba(74, 222, 128, 0.30);
}

/* ── Universal 20-pillar interest pills ──────────────────────────────────────
   Fixed per-pillar identity for BOTH the detail/share card (.cat-pill-<slug>) and
   the library/channel/trending rows (.tag-pill-<slug>). bg @12% / border @30% are
   derived from the single --pillar-<slug> hue via color-mix, so adding/retuning a
   pillar is a one-token change. The slug is set by routes/ui.py:_tag_colour /
   build_category_pills (services.interest_taxonomy.pillar_slug). */
.cat-pill-ai,.tag-pill-ai{ color:var(--pillar-ai); background:color-mix(in srgb,var(--pillar-ai) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-ai) 30%,transparent); }
.cat-pill-travel,.tag-pill-travel{ color:var(--pillar-travel); background:color-mix(in srgb,var(--pillar-travel) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-travel) 30%,transparent); }
.cat-pill-education,.tag-pill-education{ color:var(--pillar-education); background:color-mix(in srgb,var(--pillar-education) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-education) 30%,transparent); }
.cat-pill-technology,.tag-pill-technology{ color:var(--pillar-technology); background:color-mix(in srgb,var(--pillar-technology) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-technology) 30%,transparent); }
.cat-pill-business,.tag-pill-business{ color:var(--pillar-business); background:color-mix(in srgb,var(--pillar-business) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-business) 30%,transparent); }
.cat-pill-psychology,.tag-pill-psychology{ color:var(--pillar-psychology); background:color-mix(in srgb,var(--pillar-psychology) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-psychology) 30%,transparent); }
.cat-pill-news-society,.tag-pill-news-society{ color:var(--pillar-news-society); background:color-mix(in srgb,var(--pillar-news-society) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-news-society) 30%,transparent); }
.cat-pill-careers,.tag-pill-careers{ color:var(--pillar-careers); background:color-mix(in srgb,var(--pillar-careers) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-careers) 30%,transparent); }
.cat-pill-music,.tag-pill-music{ color:var(--pillar-music); background:color-mix(in srgb,var(--pillar-music) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-music) 30%,transparent); }
.cat-pill-creative-design,.tag-pill-creative-design{ color:var(--pillar-creative-design); background:color-mix(in srgb,var(--pillar-creative-design) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-creative-design) 30%,transparent); }
.cat-pill-entertainment,.tag-pill-entertainment{ color:var(--pillar-entertainment); background:color-mix(in srgb,var(--pillar-entertainment) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-entertainment) 30%,transparent); }
.cat-pill-marketing,.tag-pill-marketing{ color:var(--pillar-marketing); background:color-mix(in srgb,var(--pillar-marketing) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-marketing) 30%,transparent); }
.cat-pill-food-drink,.tag-pill-food-drink{ color:var(--pillar-food-drink); background:color-mix(in srgb,var(--pillar-food-drink) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-food-drink) 30%,transparent); }
.cat-pill-personal-development,.tag-pill-personal-development{ color:var(--pillar-personal-development); background:color-mix(in srgb,var(--pillar-personal-development) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-personal-development) 30%,transparent); }
.cat-pill-sports,.tag-pill-sports{ color:var(--pillar-sports); background:color-mix(in srgb,var(--pillar-sports) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-sports) 30%,transparent); }
.cat-pill-finance,.tag-pill-finance{ color:var(--pillar-finance); background:color-mix(in srgb,var(--pillar-finance) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-finance) 30%,transparent); }
.cat-pill-health-fitness,.tag-pill-health-fitness{ color:var(--pillar-health-fitness); background:color-mix(in srgb,var(--pillar-health-fitness) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-health-fitness) 30%,transparent); }
.cat-pill-science,.tag-pill-science{ color:var(--pillar-science); background:color-mix(in srgb,var(--pillar-science) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-science) 30%,transparent); }
.cat-pill-productivity,.tag-pill-productivity{ color:var(--pillar-productivity); background:color-mix(in srgb,var(--pillar-productivity) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-productivity) 30%,transparent); }
.cat-pill-home-diy,.tag-pill-home-diy{ color:var(--pillar-home-diy); background:color-mix(in srgb,var(--pillar-home-diy) 12%,transparent); border-color:color-mix(in srgb,var(--pillar-home-diy) 30%,transparent); }

/* Primary = the dominant interest signal (slightly stronger fill + medium weight);
   secondary pillars read a touch quieter. Paint-only, no layout shift. */
.cat-pill--primary{ font-weight:500; }
.cat-pill--primary[class*="cat-pill-"]{ background-color:color-mix(in srgb, currentColor 18%, transparent); }
.cat-pill--secondary{ opacity:.82; }

/* Channel filter items (Channels v1; restyled 2026-06-04 single-select pass).
   The Library channel filter renders each channel as a colour TRIANGLE marker
   sitting OUTSIDE the pill on its LHS, followed by a neutral outlined pill that
   reuses the .category-pill vocabulary. Channels and cat tags therefore read as
   one label family; the triangle alone differentiates a channel (user-curated)
   from a cat tag (system-assigned). The wrapper carries --topic-fill (the
   channel's hash-derived colour); it drives BOTH the triangle colour (scoped
   rule below — the channel_triangle SVG fills via currentColor) AND the pill's
   outline (.category-pill resolves color/border-color to var(--topic-fill)).
   Because the triangle is a SIBLING of the pill anchor, the active-state white
   flip (.filter-pill-active) doesn't touch it — the channel identity marker
   stays coloured in every state. Retires the old filled .channel-pill family
   (no other consumer existed; banked 2026-06-04). */
.channel-filter-item{
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}
.channel-filter-item .channel-triangle{
  width: 9px;
  height: 9px;
  flex-shrink: 0;
  color: var(--topic-fill);
}
/* Inline-with-text baseline alignment (canon #8). The brand triangle's
   geometric y-centre sits ~9.5% above its square SVG box centre, so beside
   running text (inside inline-flex `align-items:center` containers) it reads
   slightly high. translateY with a PERCENTAGE resolves against the element's
   own rendered height, so this single rule nudges the glyph down by the same
   proportion at every size (9px filter pill / 14px index card head / 22px
   detail hero). Positive = down. Chrome consumers (nav, add-channel CTA)
   omit inline_offset and are unaffected. Emitted by channel_triangle(...,
   inline_offset=true). */
.channel-triangle--inline{
  transform: translateY(9.5%);
}

/* Membership-icon triangle uses the square padded viewBox whose y-origin
   (-24, calibrated for the older 324-tall box) leaves the glyph ~9.5% above
   the SVG-box centre in the now-400-tall viewBox — so in the round icon-button
   it reads high and misaligned with the sibling row icons (pin / trash). The
   inline channel triangles compensate via .channel-triangle--inline; the
   membership icon has no inline offset, so it gets the same downward nudge
   here to sit centred in its circle. transform-origin: center per hover canon. */
.channel-membership-icon{
  transform: translateY(9.5%);
  transform-origin: center;
}

/* Nav Channels glyph — same square-viewBox high-sit as the membership icon
   (no inline offset here either), so nudge it down to centre it in the nav
   icon-button alongside the Library / Account glyphs. */
.icon-channels .channel-triangle{
  transform: translateY(9.5%);
  transform-origin: center;
}

/* Channel index card — content-entity treatment, adopting the trending
   topics visual vocabulary wholesale (channels are content entities, not
   subscription entities — canon refinement 2026-05-29). Per-channel colour
   from _tag_colour(name, []) is supplied inline as --channel-accent; the
   card wears it on BOTH the leading edge accent and the triangle, at rest.
   Asymmetric accent-edge: 3px square-left border-left + ::after 3-side
   hairline + 0 10px 10px 0 radius — identical geometry to .trending-accent-row
   and .library-row-shell. Hover lightens background only (--surface-2 →
   --surface), matching trending; NO tier-accent, NO border-colour shift,
   NO triangle colour shift. Supersedes canon #19's interaction-progressive
   model (rest=tinted-triangle-only, hover=whole-card-claims-identity) with
   the trending always-on model. */
/* Mosaic card (2026-06-06): the card is now a full-bleed collage of the
   channel's real member thumbnails with the name + meta overlaid over a bottom
   scrim — "actually visual", not a text card pretending. The .channel-mosaic
   grid tiles the served /ui/items/{id}/thumb images; data-count drives a clean
   tiling (1/2/3/4/6/9 — see _clamp_to_mosaic_ladder). Channels with no
   thumbnailable saves fall to .channel-card--empty (add-content prompt). The
   3px channel-accent left edge + ::after hairline frame are retained. */
.channel-card{
  position: relative;
  display: block;
  min-height: 150px;
  background: var(--surface-2);
  /* Edge accent is muted grey at rest, lifting to the channel colour on hover
     (2026-06-08) — the card claims its identity on interaction. */
  border-left: 3px solid var(--stripe-rest);
  border-radius: 0 10px 10px 0;
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  transition: border-left-color var(--motion-hover) ease;
}
.channel-card::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
  z-index: 4;
}
/* Hero-first collage grid (2026-06-07). The most-recent save is the large hero
   on the left ⅔; the remaining saves stack as a right-rail satellite strip.
   1px gap with the --border showing through gives a subtle hairline. Counts are
   clamped server-side to 1/2/3 (see _CHANNEL_MOSAIC_LADDER) so the grid always
   fills with no empty cells. */
.channel-mosaic{
  position: absolute;
  inset: 0;
  display: grid;
  gap: 1px;
  background: var(--border);
  grid-template-columns: 2fr 1fr;
  grid-template-rows: 1fr 1fr;
  z-index: 1;
  /* Mosaic tiles are wide-and-short (75px cards), so cover reveals only a thin
     (~20%) horizontal band of the 9:16 reel. Bias a touch HARDER than the global
     32% so that band sits higher, over the face rather than the chest. Inherited
     by .channel-mosaic-tile, which consumes it via object-position. */
  --thumb-focal-y: 26%;
}
.channel-mosaic[data-count="1"]{ grid-template-columns: 1fr; grid-template-rows: 1fr; }
.channel-mosaic[data-count="2"]{ grid-template-columns: 2fr 1fr; grid-template-rows: 1fr; }
.channel-mosaic[data-count="3"]{ grid-template-columns: 2fr 1fr; grid-template-rows: 1fr 1fr; }
/* Hero (first / most-recent tile) spans both rows of the right-rail layout. */
.channel-mosaic[data-count="3"] .channel-mosaic-tile:first-child{ grid-row: 1 / span 2; }
.channel-mosaic-tile{
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  object-fit: cover;
  display: block;
  background: var(--surface-2);
  transform-origin: center;
  transition: filter var(--motion-hover) ease, transform var(--motion-hover) ease;
}
/* Empty-channel placeholder mosaic — a legible "NO CONTENT" label centred over the
   card's --surface-2 (the failed-save placeholder svg is portrait and its text
   doesn't scale into this wide card), so an empty channel reads as a real card
   with a placeholder, not a blank box. */
.channel-mosaic--placeholder{
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Lift the glyph into the top band, clear of the bottom-pinned name/meta overlay
     (the cards are 75px, so a dead-centre mark sits under the name). */
  padding-bottom: 42px;
}
.channel-mosaic-placeholder-glyph{
  width: 20px;
  height: 20px;
  color: var(--text-meta);
  opacity: 0.42;
}
/* Cohesion wash + label scrim in one layer: a faint veil across the whole card
   unifies disparate thumbnails into one surface, deepening to a strong floor so
   the overlaid label stays legible over any frame. */
.channel-card-scrim{
  position: absolute;
  inset: 0;
  background: linear-gradient(to top,
    rgba(8, 12, 18, 0.92) 0%,
    rgba(8, 12, 18, 0.55) 28%,
    rgba(8, 12, 18, 0.12) 58%,
    rgba(8, 12, 18, 0.20) 100%);
  pointer-events: none;
  z-index: 2;
}
.channel-card-overlay{
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 3;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 12px;
  pointer-events: none;
}
/* Hover (paint-only per canon): all tiles brighten; the hero gently scales up
   (transform-origin: center, declared on the base rule) for a subtle ken-burns
   — scaling UP also hides the 1px seams. */
.channel-card:hover,
.channel-card:focus-visible{
  border-left-color: var(--channel-accent);
}
.channel-card:hover .channel-mosaic-tile,
.channel-card:focus-visible .channel-mosaic-tile{
  filter: brightness(1.08);
}
.channel-card:hover .channel-mosaic-tile:first-child,
.channel-card:focus-visible .channel-mosaic-tile:first-child{
  transform: scale(1.04);
}
.channel-card-head{
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.channel-card-head .channel-triangle{
  width: 14px;
  height: 14px;
  color: var(--channel-accent);
  flex-shrink: 0;
}
.channel-card-name{
  font-size: var(--type-card-title-size);
  font-weight: 500;
  color: var(--text);
}
.channel-card-meta{
  font-size: var(--type-meta-size);
  color: var(--text-meta);
}
/* Over the scrim the label flips to light-on-dark; the triangle keeps the
   channel colour (bright palette → legible on the dark scrim). Heavier weight +
   a soft shadow guarantee the name reads over any frame (incl. busy / captioned
   thumbnails). */
.channel-card-overlay .channel-card-name{
  color: #ffffff;
  font-weight: 600;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.55);
}
.channel-card-overlay .channel-card-meta{
  color: rgba(255, 255, 255, 0.85);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}

/* Empty channel (no thumbnailable saves) — keep the identity + an add-content
   prompt rather than a mosaic. Reverts to the padded text-card layout. */
.channel-card--empty{ background: var(--surface-2); }
.channel-card--empty:hover,
.channel-card--empty:focus-visible{ background: var(--surface); }
.channel-card-empty{
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 8px;
  min-height: 150px;
  padding: 16px;
}
.channel-card-empty-prompt{
  font-size: var(--type-meta-size);
  color: var(--text-meta);
}
/* Channel index grid — 2-column desktop / 1-column mobile, matching the
   canonical dashboard 2-col grid mechanism (repeat(2, minmax(0,1fr)) →
   1fr at the 560px breakpoint). gap: 6px matches Library's row gap
   (.library-list) and the dashboard 2-col grids — cross-surface vertical
   rhythm. minmax(0, 1fr) (not 220px) prevents long channel names from
   overflowing the column. */
.channel-cards-grid{
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px;
}
/* X-axis-only mobile rule (banked canon): collapse columns ONLY; the
   base-rule gap: 6px carries to mobile unchanged. */
@media (max-width: 560px){
  .channel-cards-grid{
    grid-template-columns: 1fr;
  }
}

/* Compact channel mosaic — the Home "Channels" overview (same cards as
   /ui/channels via _channel_mosaic_grid, tighter grid + shorter cards). Card
   height is capped to the Recents row height (59px thumb + 8/8 padding = 75px)
   so the channel strips line up with the Recents listing above them. */
.channel-cards-grid--compact{
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
.channel-cards-grid--compact .channel-card{
  min-height: 75px;
  max-height: 75px;
}
@media (max-width: 560px){
  .channel-cards-grid--compact{
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* Channels-page grid — matches the Home "Channels" overview EXACTLY (2026-06-09):
   the SAME 3-col-wide / 2-col-mobile structure + 75px height cap as
   .channel-cards-grid--compact (was a responsive auto-fill that rendered
   micro-width cards on wide screens — replaced so the two surfaces read
   identically at every width). Base .channel-card edge-accent retained (square
   LHS stripe, muted at rest → channel colour on hover). */
.channel-cards-grid--cloud{
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 6px;
}
.channel-cards-grid--cloud .channel-card{
  min-height: 75px;
  max-height: 75px;
}
@media (max-width: 560px){
  .channel-cards-grid--cloud{
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* Channels grid/list view toggle (2026-06-09) — a segmented control mirroring
   the .density-toggle/.density-btn pattern (round icon buttons; is-active =
   tier accent). Sits at the heading's far edge, like the density toggle. */
.view-toggle{
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.view-btn{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--icon-btn-size);
  height: var(--icon-btn-size);
  min-width: var(--icon-btn-size);
  min-height: var(--icon-btn-size);
  padding: 0;
  border-radius: 50%;
  border: none;
  background: transparent;
  color: var(--text-meta);
  cursor: pointer;
  line-height: 1;
}
.view-btn:hover{
  background: var(--btn-hover);
  color: var(--tier-accent, var(--accent-blue));
}
.view-btn.is-active{
  color: var(--tier-accent, var(--accent-blue));
}
.view-btn.is-active:hover{
  background: var(--btn-hover);
}
.view-icon-glyph{
  /* Solid glyphs (filled squares) read heavier than the density toggle's line
     pictograms, so they're sized DOWN to match its visual weight (not the 14px
     box size). */
  width: 12px;
  height: 12px;
  display: block;
}

/* Channel LIST view — a space-saving trending-style row per channel (mirrors
   .trending-accent-row: channel-colour left edge at rest, name + thumb strip +
   count on one line). The thumb strip replaces the trending proportion bar. */
.channel-list{ gap: 6px; }
.channel-list-row{
  display: flex;
  align-items: center;
  gap: 10px;
  /* "Row mode" — the canonical 75px row height (matches Recents / Library / Your
     people rows) so the channels list reads as a proper row, not an extra-compact
     strip. Grid mode is the distinct mosaic view. */
  min-height: 75px;
  padding: 8px 12px;
  background: var(--surface-2);
  border-left: 3px solid var(--topic-accent);
  border-radius: 0 10px 10px 0;
  position: relative;
  text-decoration: none;
  color: var(--text);
  transition: background 150ms ease;
}
.channel-list-row::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
}
.channel-list-row:hover{ background: var(--surface); }
/* The link is the row's CONTENT (name + thumb strip); the grip is its sibling, so
   the grip has no <a> ancestor and Firefox's long-press Link Preview cannot fire on
   it mid-drag. See the row comment in _channel_list.html. The link carries the
   name↔thumbs gap; the row's own gap now sits between link and grip — the same 10px
   it always was, so the layout is unchanged and only the nesting differs. */
.channel-list-link{
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  color: inherit;
}
/* Drag-to-reorder grip (2026-08-12) — LIST view only by construction (the whole
   .channel-list is display:none in grid view). A tap does nothing; a pointer-drag
   from here reorders. touch-action:none so touch-dragging the grip reorders instead
   of scrolling the page (only the grip opts out of scroll — the rest of the row
   scrolls normally). Muted at rest, brightens on hover/press. */
.channel-list-grip{
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;   /* a comfortable touch target at the row's right edge */
  height: 44px;
  margin-right: -6px;   /* pull toward the row's right padding edge */
  padding: 0;
  background: none;
  border: none;
  color: var(--text-meta);
  cursor: grab;
  /* touch-action:none is the mobile-critical bit — dragging the grip reorders
     instead of scrolling the page (only the grip opts out of scroll). */
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  transition: color 150ms ease;
}
.channel-list-grip:hover{ color: var(--text); }
.channel-list-grip:active{ cursor: grabbing; color: var(--text); }
/* Let touches land on the button, not the SVG, so the gesture target is stable. */
.channel-list-grip .grip-glyph{ display: block; pointer-events: none; }
/* The row being dragged — lifted above its siblings, softened, and cut loose from
   the row hover so it reads as "picked up". */
.channel-list-row.is-dragging{
  opacity: 0.92;
  background: var(--surface);
  box-shadow: var(--shadow-md);
  z-index: 5;
  cursor: grabbing;
}
.channel-list.is-reordering{ user-select: none; }
.channel-list.is-reordering .channel-list-row{ transition: none; }
/* Triangle in its OWN area on the left; title + meta stack to its right so both
   start at the same x (the meta eyebrow aligns to the title, not the triangle). */
.channel-list-name{
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 6px;
  /* Narrowed (was 150px) to free the row for at least 2-3 thumbs + the grip on a
     phone; the name ellipsis-clamps past this. */
  width: 118px;
  flex-shrink: 0;
  font-size: 12px;
  font-weight: 500;
}
.channel-list-name-col{
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  min-width: 0;
}
.channel-list-name .channel-triangle{ color: var(--topic-accent); flex-shrink: 0; }
/* Meta moved here from the mosaic cards — under the title (N saves · updated …). */
.channel-list-meta{
  font-size: 11px;
  font-weight: 400;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
.channel-list-name-text{
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.channel-list-thumbs{
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 4px;
  overflow: hidden;
  /* Fade the right edge so the last (clipped) thumb decays gracefully instead of
     a hard crop. */
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
}
.channel-list-thumb{
  /* Taller strip thumbs (was 28px) so they fill the 75px row instead of floating. */
  height: 44px;
  aspect-ratio: 3 / 4;
  flex-shrink: 0;
  border-radius: 4px;   /* recents-thumb radius */
  object-fit: cover;
  background: var(--surface-2);
}
.channel-list-thumbs-empty{
  font-size: 11px;
  color: var(--text-meta);
}
/* View show/hide — both layouts render server-side; the toggle flips which is
   visible (client-side, persisted). Default grid. */
.library-shell[data-channels-view="grid"] .channel-list{ display: none; }
.library-shell[data-channels-view="list"] .channel-cards-grid{ display: none; }

/* (The Home faux search field .home-search was retired 2026-06-09 — search now
   lives in the global .top-strip, so Home no longer carries its own entry.) */
.dashboard-channels-empty{ display: inline-block; text-decoration: none; }

/* Full-width add-channel card (PR C Commit 2). Single canonical add surface
   under the grid (or under the empty-state text). Consumes the shared
   .edge-accent skeleton + --muted (empty/normal) / --amber (at-cap) so the
   at-cap moment matches the Library at-limit banner. Hosted on a <button>
   (modal trigger) in empty/normal states and an <a> (upgrade) at-cap — both
   need the button/anchor UA reset (canon: non-div hosts of accent vocabulary
   reset appearance). .edge-accent's `border: 0` already neutralises UA
   borders; here we reset the remaining button/anchor chrome. */
.channels-add-card{
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  /* 24px, the same interval the homepage uses between the hero block and the row
     list — the one deliberate "these are separate things" gap. At 6px this button
     read as another row in the channel list rather than the action below it. */
  margin-top: 24px;
  padding: 12px 14px;
  font: inherit;
  font-size: var(--type-body-size);
  line-height: 1.4;
  color: var(--text);
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  /* Button vocabulary — NOT an edge-accent display container. Edge-accent is
     reserved for OUTPUT (the channel cards above display values); an
     input-generating action ("create a new channel") is a button: a fully
     rounded border + subtle fill + hover, distinct from the cards. See the
     "edge-accent = output, inputs are buttons" canon in CLAUDE.md. */
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  transition: background var(--motion-hover) ease, border-color var(--motion-hover) ease;
}
.channels-add-card:hover,
.channels-add-card:focus-visible{
  background: var(--btn-hover);
  border-color: var(--text-meta);
}
.channels-add-card-label{
  font-weight: 500;
}

/* Unified at-limit/at-cap banner — the single tier-centric banner shared by
   Library, Channels, and the MY PLAN (account) page. Replaces the three
   per-surface banner families (.library-at-limit-banner / .channels-at-cap-
   banner / .account-at-limit-banner) with one. Uses the .edge-accent + amber
   vocabulary. All-viewport (NOT mobile-gated): the at-limit moment shows at
   every width. On Library + Channels the banner OCCUPIES the tier-strip slot
   (the strip is fully suppressed when the banner shows) — .limit-banner--slot
   gives it the same 14px bottom margin the tier strip had. On the account
   page it sits in the page-level .stack (gap owns spacing) so the base
   margin: 0 applies. Paint-only amber hover (0.12 → 0.18) scoped to the anchor
   variant; the href-less <div> (Navigator, no upgrade target) is non-clickable
   and gets no hover. */
.limit-banner{
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 10px 14px 10px 11px;
  margin: 0;
  color: var(--text);
  text-decoration: none;
  font-size: var(--type-body-size);
  line-height: 1.4;
}
.limit-banner--slot{
  margin-bottom: 14px;
}
.limit-banner-copy{
  flex: 1;
  min-width: 0;
  font-weight: 500;
  position: relative;
  z-index: 1;
}
.limit-banner-arrow{
  flex-shrink: 0;
  font-size: 16px;
  color: var(--pill-processing-fg);
  position: relative;
  z-index: 1;
}
a.limit-banner.edge-accent--amber:hover{
  background: rgba(251, 191, 36, 0.18);
}
/* Channels header add affordance removed (Commit 6) — the channels-specific
   add-plus rules (the under-cap button UA reset + the at-cap grey override)
   are gone with the affordance. The .add-plus-circle shared base (Commit 1)
   remains below; it is still consumed by the tier strip. */
/* Left-flush (not centred) so it aligns with the home empty Channels section:
   hint copy + a step guide reading left-to-right, with the tier number badges +
   glyphs carrying the visual + tier colour. The 7px top padding + the 5px
   library-shell stack gap = 12px from the heading, matching the home .stack
   rhythm (gap:12px). */
.channel-empty-state{
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 7px 0 4px;
  color: var(--text-meta);
}
.channel-empty-state-title{
  font-size: var(--type-detail-size);
  font-weight: 500;
  line-height: 1.2;
  color: var(--text);
}
/* Pull-quote headline: bracketed by the Spoken glyphs, the whole line in tier
   colour so it reads as a callout, not another section title. */
/* Normal inline flow (NOT flex) so the open/close marks sit inline against the
   first/last word and the text wraps between them (2 lines), rather than each
   mark wrapping to its own line (4 lines). */
.channel-quote-title{
  display: block;
  color: var(--tier-accent, #0698ea);
  text-align: center;
  text-wrap: balance;
}
.channel-quote-mark{
  display: inline-block;
  width: 16px;
  height: auto;
  /* Raised toward cap height so the marks read as quotes hanging at the top of
     the line, not sitting on the baseline. */
  vertical-align: 0.3em;
  color: var(--tier-accent, #0698ea);
}
.channel-quote-mark--open{ margin-right: 3px; }
.channel-quote-mark--close{ margin-left: 3px; }
.channel-quote-text{ font-style: italic; }
/* margin:0 — the <p> default UA margin (~16px) would otherwise ADD to the flex
   gap (flex margins don't collapse), inflating body→guide past the home rhythm.
   Reset keeps title→body at the 12px gap and body→guide at 12px + the guide's
   16px = 28px, matching the home empty Channels section exactly. */
.channel-empty-state-body{
  font-size: var(--type-body-size);
  color: var(--text-preview);
  line-height: 1.4;
  margin: 0;
}
/* CENTRED 480px callout — ONLY the channels-list no-data state (the pull-quote
   intro). The channel-DETAIL empty state reuses these classes but stays LHS, so the
   centring is gated on the --callout modifier (applied in channels.html only). */
.channel-empty-state--callout .channel-empty-state-body{
  max-width: 480px;
  align-self: center;
  text-align: center;
  /* Even the wrapped line lengths so a stray word (e.g. a contraction) doesn't
     strand alone on a line on narrow viewports. Gracefully ignored where unsupported. */
  text-wrap: balance;
}
/* Channel-detail empty state: the Library destination (icon + "Library" + arrow)
   reads as one tier-coloured link, brightening to white on hover. */
/* Inline (NOT inline-flex) so the link TEXT ("Library →") sits on the SAME
   baseline as the surrounding body copy — then the icon is nudged to centre on
   that text line. */
.channel-empty-library-link{
  color: var(--tier-accent, #0698ea);
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
  transition: color 140ms ease;
}
.channel-empty-library-link:hover{ color: var(--text); }
.channel-empty-library-icon{ width: 14px; height: 14px; vertical-align: -0.15em; margin-right: 1px; }
/* The page guide reuses the .how-to-save system; no "Let's go" CTA (the Build
   button below is the action). margin-top 16px matches the home guide's offset. */
.how-to-channel-page{ margin-top: 16px; }

/* Empty-state onboarding hints read at body-copy brightness (--text-preview),
   NOT muted — they're the most useful words a brand-new user sees, so they match
   the channels empty-state body rather than the demoted .muted chrome. */
.empty-state-hint{ color: var(--text-preview); }
/* Library empty state only: .library-shell uses an inline gap:5px, whereas the
   home .dashboard-section uses gap:8px. In the POPULATED state the collapsed
   filter body (+ its -2px chevron adjustment) makes Library's header→list equal
   home's 8px — but the EMPTY state has no filter body, so the hint starts 3px
   higher than the home Recents hint. Nudge it down 3px so the instructional
   cards begin at the same Y on both pages. Scoped to .library-shell
   .empty-state-hint, which exists ONLY in the empty (instructional-cards) state,
   so the populated chevron rhythm is left untouched. */
.library-shell .empty-state-hint{ margin-top: 3px; }
/* ...and the hint->cards gap: .library-shell's inline gap:5px + the guide's 16px
   margin = 21px, vs home's 8px gap + 16px = 24px. Bump the guide margin 3px so
   the Library empty state matches home. Scoped to the DIRECT-child guide
   (> .how-to-save): on the Library page the guide is a direct child of
   .library-shell, but on the channels page it's nested inside .channel-empty-state,
   so the channels guide is correctly excluded. Empty-state only (no guide when
   populated). */
.library-shell > .how-to-save{ margin-top: 19px; }
/* When the guide LEADS (its intro is merged into the lead paragraph, so there's no
   separate hint above it), the big top margin above is wrong — it's now the title→
   copy gap, which should match the ~8px on every other section. margin-top:0 lands
   it at the section stack gap (home 8px); the Library variant adds 3px to absorb the
   tighter inline gap:5px (→ 8px), mirroring the retired .empty-state-hint tuning. */
.how-to-save.how-to-save--lead{ margin-top: 0; }
.library-shell > .how-to-save.how-to-save--lead{ margin-top: 3px; }

/* How-to-save — the step "first save" / "first channel" visuals on empty
   surfaces. A container (inline-size) so BOTH guides switch at the SAME breakpoint
   off their own width (they share the column, so they stack together), not the
   viewport. */
/* Fills the content column (capped at --content-max-width 980px) so the step
   guides span full-width like every other surface, instead of floating in a
   520px top-left island. container-type stays so the @container stack breakpoint
   still fires off the guide's OWN width. */
.how-to-save{ container-type: inline-size; margin-top: 16px; }
.how-to-save-steps{
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
/* Channel companion guide: 2 steps (vs the save guide's 3) to break parity. */
.how-to-save--two .how-to-save-steps{ grid-template-columns: repeat(2, 1fr); }
.how-to-save-step{
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 7px;
  padding: 14px 8px;
  background: var(--surface-2);
  border: 0.5px solid var(--border);
  border-radius: 10px;
  position: relative;
  /* Paint-only hover — ONLY on the linked variant (home), where the cards navigate
     to where saves/channels live. On the destination pages they render WITHOUT
     --link → plain message cards with no hover (so no false "clickable" affordance). */
  transition: background 140ms ease, border-color 140ms ease;
}
.how-to-save-num{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 19px;
  height: 19px;
  /* flex-shrink:0 so the circle never squashes to an oval in the stacked row
     layout (or any tight flex context). */
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--tier-accent, #0698ea);
  color: #fff;
  font-size: 0.68rem;
  font-weight: 600;
}
.how-to-save-icon{ font-size: 21px; color: var(--text-preview); line-height: 1; }
.how-to-save-brand-wrap{ display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; height: 21px; }
/* The two triangle glyphs (brand triangle in the save guide, outline channel
   triangle in the channel guide) read a touch heavy vs the other steps — tuned
   10% smaller (16 → 14.4px) so they sit at the same optical weight. */
.how-to-save-brand{ width: 14.4px; height: auto; display: block; }
/* Step glyphs (filmstrip / share / outline triangle / pen) — sized to read as
   supporting marks, not the focal point, and consistent with the 16px brand
   triangle. height:auto keeps each asset's own aspect ratio. */
.how-to-save-glyph{ width: 16px; height: auto; display: block; }
.how-to-save-glyph--tri{ width: 14.4px; }
.how-to-save-label{
  display: flex;
  flex-direction: column;
  gap: 1px;
  font-size: 0.8rem;
  font-weight: 500;
  color: var(--text-preview);
  line-height: 1.2;
}
.how-to-save-sub{ font-size: 0.7rem; font-weight: 400; color: var(--text-meta); }
/* Footer: the primary "Ready? Let's go →" CTA + the secondary desktop line on
   ONE row (wraps on narrow). The channel guide uses .how-to-save-go standalone. */
.how-to-save-footer{ display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 16px; margin-top: 12px; }
.how-to-save-footer .how-to-save-go,
.how-to-save-footer .how-to-save-desktop{ margin: 0; }
.how-to-save-go{ margin: 12px 0 0; font-size: 0.85rem; }
/* Links: the lead question reads in preview copy colour (steady); only the tier
   colour "Let's go →" CTA brightens to white on hover (paint-only). */
.how-to-save-go a{ text-decoration: none; }
.how-to-save-go-q{ color: var(--text-preview); }
.how-to-save-go-cta{ color: var(--tier-accent, #0698ea); transition: color 140ms ease; }
.how-to-save-go-cta::after{ content: " \2192"; }
.how-to-save-go a:hover .how-to-save-go-cta{ color: var(--text); }
.how-to-save-desktop{ font-size: 0.8rem; color: var(--text-preview); }
/* The lead paragraph above the cards (the empty-state intro + the desktop line,
   merged into one) reads as lead body copy; its links are tier-coloured. */
.how-to-save-lead{ margin: 0 0 12px; color: var(--text-preview); line-height: 1.5; }
.how-to-save-lead a{ color: var(--tier-accent, #0698ea); text-decoration: none; transition: color 140ms ease; }
.how-to-save-lead a:hover{ color: var(--text); }
.how-to-save-desktop a{ color: var(--tier-accent, #0698ea); text-decoration: none; transition: color 140ms ease; }
.how-to-save-desktop a:hover{ color: var(--text); }
/* Narrow CONTAINER (not viewport): stack the steps into rows so the cards never
   crush + the number circles never squash. Same breakpoint for BOTH guides — the
   --two override drops the 2-col channel grid to 1-col too, so they switch in
   lockstep. */
@container (max-width: 430px){
  .how-to-save-steps,
  .how-to-save--two .how-to-save-steps{ grid-template-columns: 1fr; }
  .how-to-save-step{ flex-direction: row; justify-content: flex-start; text-align: left; gap: 12px; }
  .how-to-save-label{ align-items: flex-start; }
}

/* Ghost section previews (home empty states) — illustrative skeletons of what a
   populated "Just saved" / "Channels" section becomes. They reuse the REAL grid
   containers (.recent-saves-list / .channel-cards-grid--compact) so the 2-up/
   3-up responsive behaviour + 75px heights match the populated layout, and the
   tiles mirror the real edge-accent chrome (left stripe + partial radius +
   hairline ::after). They brighten on hover and link to the Library / Channels
   pages. The lead <p> carries the meaning; the skeleton is decorative (the
   grid is aria-hidden + the links tabindex=-1; the eye-icon is the AT nav). */
/* margin-top:0 → the lead sits at the standard 8px section gap below the heading
   (matches the populated heading→content rhythm; the section's own gap:8px is the
   only spacer). */
.ghost-preview{ margin-top: 0; }
.ghost-preview-lead{ margin: 0 0 14px; color: var(--text-preview); line-height: 1.5; }
/* Onboarding ghost HERO grid — a small fixed 2x2 preview (vs the live 4-up) that
   reuses the real .save-hero chrome; the empty thumb shows as a brighter block. */
/* Onboarding hero grid is ALWAYS 2-up (2x1); only the list below collapses. */
.hero-grid.ghost-hero-grid{ grid-template-columns: repeat(2, minmax(0, 1fr)); }
.ghost-hero{ text-decoration: none; }
/* Schematic chrome: a PERSISTENT tier accent edge (.ghost-preview prefix outweighs
   the live .save-hero rules later in the file). The only hover affordance is the
   background-lightness lift inherited from .save-hero:hover — these link to the
   Library, so the lift signals that. */
.ghost-preview .ghost-hero{ border-left-color: var(--tier-accent); }
/* Reduced cards: a short placeholder thumb (not the full 16:9) + the play shown at rest. */
.ghost-hero .save-hero__thumb{ background: var(--surface); aspect-ratio: auto; height: 92px; }
.ghost-hero .save-hero__play{ opacity: 1; }
/* Onboarding list: 2-up on wide, stacked (1-up) on narrow — both rows always show. */
.recent-saves-list.ghost-saves-list{ grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 560px){
  .recent-saves-list.ghost-saves-list{ grid-template-columns: minmax(0, 1fr); }
}

/* Save-row skeletons — mirror .recent-save-row exactly (edge-accent, 75px,
   hairline). Tier-coloured stripe at rest; brighten the fill on hover. A dummy
   meta title + text tags give them life; the summary body stays skeleton. */
.ghost-save-row{
  display: flex; align-items: flex-start; gap: 10px;
  padding: 8px 12px; box-sizing: border-box; min-height: 75px;
  background: var(--surface-2);
  border-left: 3px solid var(--tier-accent, #0698ea);
  border-radius: 0 10px 10px 0;
  position: relative; text-decoration: none;
  transition: background 150ms ease;
}
.ghost-save-row::after{
  content: ''; position: absolute; inset: 0;
  border-top: 0.5px solid var(--border); border-right: 0.5px solid var(--border); border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0; pointer-events: none;
}
/* Hover lifts only the background (like the channels rows) — signals these link
   to the Library; the persistent tier stripe stays put. */
.ghost-save-row:hover{ background: var(--surface); }
.ghost-thumb{
  flex-shrink: 0; width: 44px; height: 59px; border-radius: 4px;
  background: var(--surface);
}
.ghost-body{ flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; padding-top: 2px; }
.ghost-title{
  /* --text-preview (not the brighter --text) so the onboarding ghost titles sit
     QUIETER than the section lead/heading — the headers stay the focus. */
  font-size: var(--type-preview-size); font-weight: 500; color: var(--text-preview);
  line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ghost-line{ height: 6px; border-radius: 3px; background: var(--surface); width: 100%; }
.ghost-line--short{ width: 80%; }
.ghost-tags{ display: flex; gap: 5px; margin-top: 3px; }
.ghost-tag{
  font-size: 9px; padding: 2px 7px; border-radius: 12px; font-weight: 400;
  line-height: 1.4; border: 0.5px solid;
}
.ghost-tag--c1{ color: var(--tag-blue); background: color-mix(in srgb, var(--tag-blue) 12%, transparent); border-color: color-mix(in srgb, var(--tag-blue) 38%, transparent); }
.ghost-tag--c2{ color: var(--tag-green); background: color-mix(in srgb, var(--tag-green) 12%, transparent); border-color: color-mix(in srgb, var(--tag-green) 38%, transparent); }
.ghost-tag--c3{ color: var(--tag-purple); background: color-mix(in srgb, var(--tag-purple) 12%, transparent); border-color: color-mix(in srgb, var(--tag-purple) 38%, transparent); }

/* Channel skeletons — mirror .channel-card (edge-accent, 75px, hairline) with a
   per-tile channel colour (multi-colour); brighten the fill on hover. A coloured
   play triangle + a dummy name mirror the real card overlay. */
.ghost-channel-card{
  position: relative; display: block; min-height: 75px; max-height: 75px;
  background: var(--surface-2);
  border-left: 3px solid var(--ghost-ch, var(--stripe-rest));
  border-radius: 0 10px 10px 0; overflow: hidden; text-decoration: none;
  transition: background 150ms ease;
}
.ghost-channel-card::after{
  content: ''; position: absolute; inset: 0;
  border-top: 0.5px solid var(--border); border-right: 0.5px solid var(--border); border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0; pointer-events: none;
}
.ghost-channel-card:hover{ background: var(--surface); }
.ghost-channel-card--c1{ --ghost-ch: var(--tag-coral); }
.ghost-channel-card--c2{ --ghost-ch: var(--tag-blue); }
.ghost-channel-card--c3{ --ghost-ch: var(--tag-green); }
.ghost-channel-overlay{
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 3;
  display: flex; flex-direction: column; gap: 6px; padding: 10px 12px;
}
.ghost-channel-head{ display: flex; align-items: center; gap: 8px; min-width: 0; }
.ghost-channel-head .channel-triangle{ width: 14px; height: 14px; color: var(--ghost-ch); flex-shrink: 0; }
.ghost-channel-name{ font-size: var(--type-card-title-size); font-weight: 600; color: var(--text-preview); min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ghost-channel-meta{ height: 6px; width: 52px; border-radius: 3px; background: var(--surface); }
/* Onboarding titles: quiet (--text-preview) at rest, brighten to full --text on
   hover so the key message pushes forward when the user engages with a card. */
.ghost-title, .ghost-channel-name{ transition: color 150ms ease; }
.ghost-hero:hover .ghost-title,
.ghost-save-row:hover .ghost-title,
.ghost-channel-card:hover .ghost-channel-name{ color: var(--text); }

/* Channels nav icon — uses the canonical channel_triangle macro at 18px
   to match the nav icon size canon (parallel to other .nav .icon-btn .ti
   font icons). Triangle inherits .icon-btn rest/hover colours via
   currentColor. */
.nav .icon-channels .channel-triangle{
  width: 18px;
  height: 18px;
}

/* Channel-membership action button glyph (per-row affordance) — sits in
   the .library-bottom-actions cluster alongside Pin/Trash. Sizing now
   set explicitly via size_px=18 on triangle_membership_icon (which
   forwards to play_triangle's width/height SVG attrs); no CSS sizing
   rule needed. Fill-state (solid = in-channels, outline = not-in-
   channels) is owned by the macro's variant param. If a future tweak
   needs to scope styling here, target .channel-membership-icon. */

/* Add-to-channel action sheet — modal-overlay sibling rendered once per
   page in items.html (Commit C) / channel_detail.html (Commit D). Reuses
   .modal-overlay + .modal-card vocabulary; channel rows are stacked
   labels with leading checkboxes. */
.channel-action-sheet-list{
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.channel-action-sheet-row{
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border-radius: 6px;
  cursor: pointer;
  font-size: var(--type-body-size);
}
.channel-action-sheet-row:hover{
  background: var(--btn-hover);
}
.channel-action-sheet-row input[type="checkbox"]{
  margin: 0;
  flex-shrink: 0;
}

/* Channel detail page hero icon — scoped 22px channel_triangle in the
   header row. Per-channel colour is supplied inline on the span
   (color: var(--tag-{colour})) from _tag_colour(name, []) — same source
   as the index card + filter pill (canon #13/#19, content-entity
   treatment 2026-05-29). The triangle picks it up via currentColor. No
   tier-accent — channel surfaces carry channel identity, not tier state. */
.channel-detail-hero-icon{
  display: inline-flex;
  align-items: center;
  /* Rests pointing LEFT — 180° from the channel triangle's play direction — so it
     reads as a "back" arrow AT REST (the link goes back to the channels index),
     discoverable without hover (incl. on touch, where hover never fires). On
     landing it animates right→left (the teaching spin), showing the channel
     triangle becoming a back arrow. transform-origin guards off-centre rotation. */
  transform-origin: center;
  transform: rotate(180deg);
  transition: filter var(--motion-hover) ease;
  animation: hero-triangle-flip 420ms ease;
}
@keyframes hero-triangle-flip{
  from{ transform: rotate(0deg); }
  to{ transform: rotate(180deg); }
}
/* Reduced-motion: no spin — just render the back-pointing rest state. */
@media (prefers-reduced-motion: reduce){
  .channel-detail-hero-icon{ animation: none; }
}
.channel-detail-hero-icon .channel-triangle{
  width: 22px;
  height: 22px;
}
/* Hero triangle links back to the channels index — a nav-habit affordance. The
   rotation is now the REST state (back arrow), so hover gives a paint-only
   brighten (no scale, no underline); the back rotation is untouched. */
.channel-detail-hero-link{
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  color: inherit;
}
.channel-detail-hero-link:hover .channel-detail-hero-icon,
.channel-detail-hero-link:focus-visible .channel-detail-hero-icon{
  filter: brightness(1.2);
}

/* Channel detail Edit trigger — meta-dim → bright on hover, matching
   .account-card-action vocabulary but scoped here so the margin-left:auto
   from that rule doesn't push Edit away from the channel name. Inline
   placement, sentence case, 11px/400. Button element so explicit
   background/border reset is required. */
.channel-detail-edit-trigger{
  margin-left: 0;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  font-size: var(--type-meta-size);
  font-weight: 400;
  color: var(--text-meta);
  text-decoration: none;
  line-height: 1.2;
  transition: color 140ms ease;
}
.channel-detail-edit-trigger:hover,
.channel-detail-edit-trigger:focus-visible{
  color: var(--text);
  outline: none;
}

/* Reel / Post / Story / Audio / Carousel — neutral data pill.
   Identifies the source artefact type; not a state-bearing pill, so no dot. */
.source-kind-pill{
  align-self: flex-start;
  width: fit-content;
  display: inline-flex;
  align-items: center;
  padding: 3px 9px;
  min-height: 22px;
  border-radius: 10px;
  border: 0;
  background: var(--pill-queued-bg);
  color: var(--pill-queued-fg);
  font-size: var(--type-meta-size);
  font-weight: 400;
  line-height: 1.1;
  text-decoration: none;
  transition: filter 140ms ease;
}

.source-kind-pill:hover{
  filter: brightness(1.08);
  text-decoration: none;
}

.source-mechanics{
  margin-top: 0;
  gap: 6px;
}

/* Source capture state pills (Caption / Transcription / Extracted Text / Track).
   Captured = green text-only pill. Missing = slate text-only pill.
   Text-only: state read through tint + text colour (no dot, no icon) — matches
   the product-wide Done-pill canonicalisation (status pills carry no dot). */
.source-pill{
  display: inline-flex;
  align-items: center;
  padding: 3px 9px;
  min-height: 22px;
  border-radius: 10px;
  border: 0;
  font-size: var(--type-meta-size);
  font-weight: 400;
  line-height: 1.1;
  white-space: nowrap;
}

.source-pill--captured{
  background: var(--pill-done-bg);
  color: var(--pill-done-fg);
}

.source-pill--missing{
  background: var(--pill-queued-bg);
  color: var(--pill-queued-fg);
}

.hashtag{
  color: var(--accent-blue);
  font-weight: 600;
}

.mention{
  color: var(--accent-blue);
  font-weight: inherit;
  text-decoration: none;
}

.mention:hover{
  text-decoration: underline;
}

.source-link{
  color: var(--accent-blue);
  text-decoration: none;
  overflow-wrap: anywhere;
  word-break: break-word;
}

.source-link:hover{
  text-decoration: underline;
}

.status-dot{
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #9aa0a6;
  display: inline-block;
}

.status-pill.status-done{
  background: transparent;
  /* Outline same colour as the label (--text-meta) so the pill reads as one
     cohesive element; 1.5px weight matches the sibling brand outline icons. */
  border: 1.5px solid var(--text-meta);
  color: var(--text-meta);
  font-size: var(--type-meta-size);
  padding: 2px 8px;
  border-radius: 999px;
}
.status-pill.status-processing{
  background: var(--pill-processing-bg);
  color: var(--pill-processing-fg);
}
.status-pill.status-failed{
  background: var(--pill-failed-bg);
  color: var(--pill-failed-fg);
}
/* Custom fail glyph (inline SVG, currentColor) sits inline with the code text. */
.status-pill-glyph{ width: 0.95em; height: 0.95em; vertical-align: -0.14em; margin-right: 4px; }
.status-pill.status-queued{
  background: var(--pill-queued-bg);
  color: var(--pill-queued-fg);
}

.source-unavailable-card{
  gap: 10px;
  border-color: var(--border);
  padding: 16px;
  margin-bottom: 12px;
}
/* Failure actions: a clear primary/secondary pair on one wrapping row — Connect
   (solid brand blue, the recommended fix; opens the wizard) leads; Upload instead
   stays the neutral default button (secondary). Both inherit the base button
   pill/padding so they size consistently. */
.source-unavailable-actions{ display: flex; flex-direction: column; align-items: stretch; gap: 8px; }
/* Error-card action buttons conform to the app's PRIMARY-CTA vocabulary (the
   #process-btn / .channels-add-card language): a 10px rounded-rect with comfortable
   proportions, NOT the 999px utility pill — so the recovery reads as a real button,
   not a stubby lozenge. */
/* Error / recovery action buttons (Reprocess, Upload instead, Add audio) all share the
   manual-add Process button treatment: a 10px rounded-rect with the --surface-2 muted
   fill + hairline border and a muted label that brightens to white on hover — the fill
   stays CONSTANT so the hover doesn't scream. Connect (the recommended primary fix) is
   the one exception: a solid brand-blue fill (the single armed/recommended CTA). */
.source-unavailable-connect-btn,
.source-unavailable-upload-btn,
.failed-error-retry-btn,
.add-audio-btn{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  /* 40px, shorter than the manual-add Process button (52px) — Process is deeper only
     to line up with the input field/cards directly adjacent to it on that page. */
  min-height: 40px;
  padding: 10px 18px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text-meta);
  font-size: var(--type-body-size);
  font-weight: 500;
  cursor: pointer;
  transition: background 140ms ease, color 140ms ease;
}
.source-unavailable-upload-btn:hover,
.failed-error-retry-btn:hover,
.add-audio-btn:hover{
  background: var(--surface-2);   /* constant muted fill; only the label brightens */
  color: var(--text);
}
.source-unavailable-connect-btn{
  background: #0698ea;
  border-color: transparent;
  color: #fff;
  transition: filter 140ms ease;
}
.source-unavailable-connect-btn:hover{ filter: brightness(0.92); background: #0698ea; color: #fff; }
.failed-error-retry-btn .ti{ font-size: 16px; line-height: 1; }
.source-unavailable-upload-btn .upload-glyph{ height: 18px; width: auto; flex-shrink: 0; }

/* Generic failed-save card: warm explanation + a stable support code chip
   (RN-xxx) + a collapsible raw detail. Mirrors the source-unavailable card frame. */
/* Identical frame to .source-unavailable-card so EVERY error state presents the
   same: a heading + reason in the Summary position + support link. The RN code is
   shown ONCE (top status pill), never repeated in the card. */
.failed-error-card{ gap: 10px; border-color: var(--border); padding: 16px; margin-bottom: 12px; }
/* The RN-xxx code now renders via the error_code_pill macro (= .status-pill
   .status-failed) so the pill, the failed card, and the legend share ONE
   presentation (radius / weight / glyph). The standalone .error-code-chip is
   retired. Letter-spacing nudge keeps the code legible at pill size. */
/* The RN-xxx code chip drops the washed half-red FILL for a clean neutral hairline
   (matching the surrounding panels) — the red glyph + red code text carry the error
   signal without a tinted blob. Scoped to .error-code-pill so row "Failed" status
   pills keep their filled treatment. Specificity beats .status-pill.status-failed. */
.status-pill.status-failed.error-code-pill{
  background: transparent;
  border: 0.5px solid var(--border);
}
.error-code-pill{ letter-spacing: 0.02em; }
.failed-error-action{ color: var(--text-preview); }
/* Retry CTA row — mirrors .source-unavailable-actions so the "Try re-saving it"
   pill matches the "Upload instead" pill (same base button pill/padding). */
.failed-error-actions{ display: flex; flex-direction: column; align-items: stretch; gap: 8px; }
/* Direct "Email support" mailto — the route to the support process. TIER-COLOURED
   envelope glyph + text (the glyph leads), so the route reads as a single coloured
   affordance. It now rides INLINE at the end of the reason sentence (not a lone
   line), so it flows with the surrounding text (baseline-aligned, no top margin) and
   never wraps mid-label. No text I-beam (a clickable route, not selectable copy);
   brightens slightly on hover. */
.failed-error-support-link{
  display: inline-flex;
  align-items: center;
  vertical-align: baseline;
  gap: 6px;
  white-space: nowrap;
  font-size: 13px;
  font-weight: 500;
  /* Brand blue, NOT the tier accent — a failed save / support route is a global
     error affordance, not a tier-personalised one. */
  color: #0698ea;
  text-decoration: none;
  user-select: none;
  -webkit-user-select: none;
  transition: filter var(--motion-hover, 140ms) ease;
}
.failed-error-support-glyph{
  width: 14px;
  height: auto;
  flex-shrink: 0;
  transform: translateY(1px);
}
.failed-error-support-link:hover{ filter: brightness(1.12); }

/* Add-audio recovery button — the full-width variant of the shared recovery-button
   treatment above (it sits alone in the SOURCE card, so it spans the column). */
.add-audio-form{ margin-top: 8px; }
.add-audio-btn .add-audio-glyph{ height: 18px; width: auto; flex-shrink: 0; }

input, textarea, button, select{ font: inherit; }

input, textarea, select{
  width: 100%;
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 10px 12px;
  background: var(--control-bg);
  color: var(--text);
}

input[type="radio"],
input[type="checkbox"]{
  width: auto;
  padding: 0;
  border: none;
  background: transparent;
  border-radius: 0;
}

textarea{
  max-width: 100%;
  resize: vertical;
}

input:focus, textarea:focus, select:focus{
  outline: none;
  border-color: var(--border-2);
}

.input-ready{
  border-color: var(--accent-blue-soft);
  box-shadow: 0 0 0 1px rgba(6, 152, 234, 0.2);
}

/* ------------------------------------------------------------------
   Auth pages (/auth/login, /auth/forgot, /auth/reset) — design language v1
   Plain outer card; child inputs carry the asymmetric accent stripe
   (matches the paste input on /ui/items/new). The Google SSO button is
   the one place brand-coloured icons are correct — its multicolour G
   stays in original treatment.
   ------------------------------------------------------------------ */

.auth-shell{
  min-height: 0;
  display: grid;
  justify-items: center;
  align-items: start;
  padding: clamp(24px, 7vh, 72px) 0 20px;
}

/* One centered column holds the brand header + welcome + the sign-in panel so
   they share a left edge and width (the panel is no longer the only child). */
.auth-col{
  width: min(420px, 100%);
  display: flex;
  flex-direction: column;
}

/* Pre-auth brand header: the in-app wordmark + a hairline rule (the HR mirrors
   the top strip's bottom border), MINUS the plus + search. */
.auth-header{
  padding-bottom: 14px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 22px;
}
.auth-brand{
  display: inline-flex;
  color: var(--text);
  text-decoration: none;
}
.auth-brand .brand-logo{
  width: 132px;
  height: auto;
  display: block;
}

/* Welcome lockup — a calm two-line hero between the brand rule and the panel
   (login page only). Title bold, tagline lighter; same size so they read as one
   sentence split across two lines. */
.auth-welcome{
  margin-bottom: 22px;
}
.auth-welcome-title{
  font-size: 1.9rem;
  line-height: 1.18;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.01em;
}
.auth-welcome-tagline{
  font-size: 1.9rem;
  line-height: 1.18;
  font-weight: 400;
  color: var(--text-preview);
  letter-spacing: -0.01em;
}

.auth-panel{
  width: min(420px, 100%);
  border: 0.5px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  padding: 20px;
  display: grid;
  gap: 12px;
}

.auth-title{
  margin: 0 0 2px;
  text-align: left;
  font-size: var(--type-card-title-size);
  line-height: var(--type-card-title-line);
  letter-spacing: var(--type-card-title-track);
  text-transform: uppercase;
  font-weight: 400;
}

/* SSO buttons — outlined, full-width. Brand icon left, label centred. */
.auth-provider{
  width: 100%;
  min-height: 40px;
  border-radius: 10px;
  border: 0.5px solid var(--border);
  background: var(--surface);
  color: var(--text);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 8px 12px;
  font-size: var(--type-body-size);
  font-weight: 400;
  text-decoration: none;
  transition: background 140ms ease;
}

.auth-provider:hover,
.auth-provider:focus-visible{
  background: var(--surface-2);
  text-decoration: none;
  outline: none;
}

.auth-provider:disabled,
.auth-provider.is-disabled{
  opacity: 0.55;
  cursor: not-allowed;
}

.auth-provider.is-disabled:hover{
  background: var(--surface);
}

/* "Continue as <user>" row — keeps the split-row layout (avatar+copy left,
   provider mark right) since it's information-rich. */
.auth-provider-account{
  justify-content: space-between;
  min-height: 52px;
}

.auth-provider-left{
  display: inline-flex;
  align-items: center;
  gap: 10px;
}

.auth-avatar{
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: #152852;
  color: #8fb7ff;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  font-weight: 700;
}

.auth-provider-copy{
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
}

.auth-provider-copy strong{
  font-size: var(--type-body-size);
  line-height: 1.1;
  font-weight: 500;
}

.auth-provider-copy small{
  color: var(--text-meta);
  font-size: var(--type-meta-size);
  line-height: 1;
}

.auth-provider-mark{
  width: 24px;
  height: 24px;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
}

.auth-provider-mark-google{
  color: #2a62f4;
  background: rgba(42, 98, 244, 0.12);
}

/* Brand icons keep their original colours — SSO buttons are the one
   sanctioned exception to the "muted glyphs everywhere else" rule. */
.auth-provider-google-icon{
  width: 18px;
  height: 18px;
  display: block;
  flex: 0 0 18px;
}

/* OR divider — uppercase, meta-dim, matches the Add A Save card's divider. */
.auth-divider{
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--text-meta);
}

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

.auth-divider span{
  font-size: var(--type-meta-size);
  font-weight: 400;
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.auth-form{
  display: grid;
  gap: 6px;
}

/* Field labels stay above-input for fresh-input forms. Meta-dim. */
.auth-label{
  color: var(--text-meta);
  font-weight: 400;
  font-size: var(--type-meta-size);
  margin-top: 4px;
}

.auth-form > .auth-label:first-of-type{
  margin-top: 0;
}

/* Input row — asymmetric accent stripe identical to /ui/items/new paste input.
   At rest: var(--stripe-rest). On focus-within: signature blue.
   Login is unauthenticated, so the stripe uses callout-accent (no tier ladder
   to source from). */
.auth-input-wrap{
  position: relative;
  border: 0;
  border-left: 3px solid var(--stripe-rest);
  border-radius: 0 10px 10px 0;
  background: var(--card-rest);
  transition: border-left-color 140ms ease, background 140ms ease;
}

.auth-input-wrap::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
}

.auth-input-wrap:focus-within{
  border-left-color: var(--callout-accent);
}

.auth-input{
  width: 100%;
  border: 0;
  background: transparent;
  border-radius: 0;
  padding: 10px 12px 10px 11px;
  outline: none;
  box-shadow: none;
  color: var(--text);
  font-size: var(--type-body-size);
  position: relative;
  z-index: 1;
}

.auth-input:focus{
  border-color: transparent;
  box-shadow: none;
}

.auth-input::placeholder{
  color: var(--text-meta);
}

.auth-input:-webkit-autofill,
.auth-input:-webkit-autofill:hover,
.auth-input:-webkit-autofill:focus,
.auth-input:-webkit-autofill:active{
  -webkit-text-fill-color: var(--text);
  transition: background-color 9999s ease-out 0s;
  box-shadow: 0 0 0px 1000px transparent inset;
}

/* Password row reserves space on the right for the visibility toggle. */
.auth-input-wrap-password .auth-input{
  padding-right: 44px;
}

/* Password visibility toggle — same vocabulary as filter cog / eye icons
   elsewhere: transparent at rest, btn-hover background on hover, glyph
   transitions from meta-dim to bright. */
.auth-input-toggle{
  position: absolute;
  top: 0;
  bottom: 0;
  right: 6px;
  margin-block: auto;
  width: 28px;
  height: 28px;
  border: 0;
  border-radius: 50%;
  padding: 0;
  background: transparent;
  color: var(--text-meta);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 2;
  transition: background 140ms ease, color 140ms ease;
}

.auth-input-toggle:hover{
  background: var(--btn-hover);
  color: var(--text);
}

.auth-input-toggle:focus-visible{
  outline: 1px solid var(--callout-accent);
  outline-offset: 1px;
}

.auth-input-toggle-eye{
  /* 16.2px = 18px - 10% (felt a touch large across surfaces, 2026-06-10). */
  width: 16.2px;
  height: 16.2px;
  display: block;
  /* The eye outline stroke reaches the viewBox's right edge; the SVG viewport
     would otherwise clip the rounded tip (the "flattened RHS edge"). */
  overflow: visible;
}

/* Primary CTA — disabled-until-ready (grey → solid blue), 10px radius
   (NOT pill), 40px height. Same activation vocabulary as the Process
   button on /ui/items/new — .cta-ready is the shared "input passes
   validation" class; JS toggles both .cta-ready and the [disabled]
   HTML attribute in sync. Server-side validation remains authoritative;
   client-side gating is a UX affordance only. */
.auth-continue{
  margin-top: 8px;
  width: 100%;
  min-height: 40px;
  border: 0;
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text-meta);
  font-size: var(--type-body-size);
  line-height: 1.1;
  font-weight: 500;
  cursor: not-allowed;
  transition: background 140ms ease, color 140ms ease, filter 140ms ease;
}

.auth-continue.cta-ready{
  background: var(--callout-accent);
  color: #ffffff;
  cursor: pointer;
}

.auth-continue.cta-ready:hover,
.auth-continue.cta-ready:focus-visible{
  background: var(--callout-accent);
  filter: brightness(0.92);
  outline: none;
}

/* [disabled] HTML attribute wins over .cta-ready — JS keeps them in
   sync, but a stale class never produces an active appearance. */
.auth-continue[disabled],
.auth-continue.cta-ready[disabled]{
  background: var(--surface-2);
  color: var(--text-meta);
  cursor: not-allowed;
  filter: none;
}

/* Footer row — "New here? Create account" / "Already have an account? Sign in" */
.auth-mode-row{
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-size: var(--type-meta-size);
  color: var(--text-meta);
  margin-top: 4px;
}

.auth-mode-row a{
  color: var(--callout-accent);
  text-decoration: none;
  font-weight: 400;
  transition: filter 140ms ease;
}

.auth-mode-row a:hover,
.auth-mode-row a:focus-visible{
  filter: brightness(1.15);
  text-decoration: none;
  outline: none;
}

/* Forgot password — meta-dim at rest, brightens on hover, no underline.
   Same treatment as .account-card-action (Change password on /ui/upgrade). */
.auth-forgot-row{
  display: flex;
  justify-content: flex-end;
  margin-top: 2px;
}

.auth-forgot-row a{
  color: var(--text-meta);
  font-size: var(--type-meta-size);
  font-weight: 400;
  text-decoration: none;
  transition: color 140ms ease;
}

.auth-forgot-row a:hover,
.auth-forgot-row a:focus-visible{
  color: var(--text);
  text-decoration: none;
  outline: none;
}

button{
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 10px 12px;
  background: var(--btn);
  color: var(--text);
  cursor: pointer;
}

button:hover{ background: var(--btn-hover); }

.btn-text{
  border: 0;
  background: transparent;
  color: var(--muted);
  padding: 0;
  min-width: auto;
  font-size: var(--type-body-size);
  font-weight: 400;
  line-height: 1.2;
  text-decoration: none;   /* a text BUTTON — <a class="btn-text"> must not underline */
}

.btn-text:hover{
  background: transparent;
  color: var(--text);
}

/* Analytics "Errors experienced": affected-user line recedes (muted); the Gmail
   compose links stay muted with no underline, brightening on hover. */
.error-users{ color: var(--text-meta); }
.error-users a{ color: var(--text-meta); text-decoration: none; }
.error-users a:hover{ color: var(--text); text-decoration: underline; }

.page-primary-cta{
  display: flex;
  width: 100%;
  min-height: 38px;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  border: 1px solid var(--accent-blue);
  background: var(--accent-blue);
  color: #ffffff;
  text-decoration: none;
  font-size: var(--type-body-size);
  font-weight: 500;
  line-height: 1.1;
  margin-bottom: 12px;
}

.page-primary-cta:hover{
  background: #0588d2;
  border-color: #0588d2;
  text-decoration: none;
}

.item-detail-page .page-primary-cta{
  width: auto;
  display: inline-flex;
}

.page-primary-cta-upgrade{
  border-color: var(--accent-green);
  background: var(--accent-green);
}

.page-primary-cta-upgrade:hover{
  background: var(--accent-green-strong);
  border-color: var(--accent-green-strong);
}

.page-primary-cta-disabled,
.page-primary-cta[aria-disabled="true"]{
  border-color: var(--border);
  background: var(--surface-2);
  color: var(--muted);
  cursor: not-allowed;
  pointer-events: none;
}

.admin-logout-cta{
  margin-bottom: 0;
  border: 0;
  background: var(--accent-red);
  color: #fff;
}

.admin-logout-cta:hover{
  border: 0;
  background: #cf2430;
  color: #fff;
}

.admin-exit-cta{
  margin-bottom: 0;
}

/* Save commit button — stripe-bearing row in signature blue (same geometry
   and tokens as .admin-action-row--exit at the bottom of admin pages).
   Saturated green / red full-fill button styling is deprecated product-wide
   for commit/exit/lock actions; the stripe + 6% tint pattern carries the
   semantic colour without competing for attention with a saturated pill. */
.admin-unlock-cta{
  position: relative;
  border: 0;
  border-left: 3px solid var(--callout-accent);
  border-radius: 0 10px 10px 0;
  padding: 10px 14px;
  background: rgba(33, 150, 243, 0.06);
  color: var(--callout-accent);
  font-size: var(--type-body-size);
  font-weight: 400;
  cursor: pointer;
  text-align: left;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  width: 100%;
  transition: background var(--motion-hover) ease;
}

.admin-unlock-cta::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
}

.admin-unlock-cta:hover{
  background: rgba(33, 150, 243, 0.10);
  color: var(--callout-accent);
}

.admin-key-input{
  border-color: rgba(6, 152, 234, 0.55) !important;
  background: none !important;
  background-color: transparent !important;
  background-image: none !important;
  box-shadow: none !important;
  color: var(--text);
  -webkit-appearance: none;
  appearance: none;
}

.admin-key-wrap{
  position: relative;
}

.admin-key-wrap .admin-key-input{
  padding-right: 44px;
}

.admin-key-toggle{
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  width: 30px;
  height: 30px;
  border: 0 !important;
  border-radius: 0 !important;
  padding: 0 !important;
  margin: 0;
  background: transparent !important;
  box-shadow: none !important;
  -webkit-appearance: none;
  appearance: none;
  color: rgba(255, 255, 255, 0.8);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 2;
}

.admin-key-toggle:hover{
  color: var(--text);
  background: transparent !important;
}

.admin-key-toggle:focus-visible{
  outline: 1px solid var(--accent-blue);
  outline-offset: 1px;
}

.admin-key-toggle-eye{
  width: 20px;
  height: 20px;
  display: block;
}

.admin-key-input::placeholder{
  color: var(--muted);
}

.admin-key-input:-webkit-autofill,
.admin-key-input:-webkit-autofill:hover,
.admin-key-input:-webkit-autofill:focus,
.admin-key-input:-webkit-autofill:active{
  -webkit-text-fill-color: var(--text) !important;
  -webkit-box-shadow: 0 0 0 1000px transparent inset !important;
  box-shadow: 0 0 0 1000px transparent inset !important;
  background-color: transparent !important;
  transition: background-color 9999s ease-in-out 0s;
}

.admin-econ-root{
  width: 100%;
}

.admin-auth-form{
  width: 100%;
  max-width: none;
}

.admin-auth-form input[type="checkbox"]{
  width: auto;
  flex: 0 0 auto;
  padding: 0;
  accent-color: var(--accent-blue);
}

.admin-auth-form label.row{
  flex-wrap: nowrap;
}

#process-btn{
  min-height: 38px;
  min-width: 0;
  width: 100%;
  justify-content: center;
}

.cta-ready{
  border-color: var(--accent-blue-soft);
  background: var(--accent-blue-bg);
}

.cta-ready:hover{
  background: var(--accent-blue-bg-hover);
}

.icon-btn{
  border: none;
  background: transparent;
  width: var(--icon-btn-size);
  height: var(--icon-btn-size);
  min-width: var(--icon-btn-size);
  min-height: var(--icon-btn-size);
  padding: 0;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  text-decoration: none;
}

.icon-btn:hover{
  background: var(--btn-hover);
}

.nav .icon-btn{
  color: var(--text-meta);
  transition: color var(--motion-hover) ease, background var(--motion-hover) ease;
}

.nav .icon-btn:hover,
.nav .icon-btn:focus-visible{
  color: var(--text);
}

.nav .icon-btn .ti{
  font-size: 18px;
  line-height: 1;
}

/* Detail page meta row icons (share / reprocess / archive · restore / trash)
   render via Tabler webfont — single source uniformly weighted. Glyph size
   matches .nav .icon-btn so the meta row reads visually consistent with the
   header iconography. Per-icon overrides are disallowed; the four icons all
   inherit the canonical .icon-btn vocabulary (rest var(--text-meta), hover
   var(--text) for .icon-muted; hover var(--pill-failed-fg) for .icon-danger). */
.item-detail-page .item-actions .icon-btn .ti{
  /* 16px to match the slightly-reduced brand glyphs (share/trash at 14px). */
  font-size: 16px;
  line-height: 1;
}

.item-retry-btn-failed{
  color: var(--text);
  background: transparent;
  border: 1px solid rgba(239, 68, 68, 0.5);
}

.item-retry-btn-failed:hover{
  color: var(--text);
  background: rgba(239, 68, 68, 0.12);
  border-color: rgba(239, 68, 68, 0.66);
}

.icon-muted{
  color: var(--text-meta);
  transition: color var(--motion-hover) ease, background var(--motion-hover) ease;
}

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

/* Preview-size utility — for content that wants one step below body
   (admin debug rows, fine-print scaffolding). Always pair with the
   token, never with a literal 12px. */
.text-preview{
  font-size: var(--type-preview-size);
}


.icon{
  width: var(--icon-glyph-size);
  height: var(--icon-glyph-size);
  display: block;
}

/* Zoom-shave guard (2026-06-20). The in-app Display zoom (`zoom:N%` on <html>)
   rasterises every glyph at a scaled size; an inline <svg>'s default
   `overflow:hidden` viewport then sub-pixel-clips any stroke/path sitting at the
   viewBox edge (the brand outline glyphs use stroke-width 36 paths that reach the
   boundary), showing as a "shaved" edge at non-100% zoom. These are fixed-size
   glyphs centred in roomy icon buttons, so letting the edge paint outside the
   nominal box is strictly additive — it reveals the clipped pixels, never shifts
   layout. NOT the same as the 19f sparkline pin: that responsive SVG clips at its
   CONTAINER (.behaviour-line-chart, overflow:hidden + position pin), so it is
   unaffected by this rule on the svg box. */
.icon, .icon-title, .icon-lg, .nav-icon-glyph, .view-icon-glyph,
.trash-icon, .share-icon, .download-icon, .more-icon,
.status-pill-glyph, .play-triangle, .channel-triangle{
  overflow: visible;
}

/* Brand-supplied nav glyphs (Library / My Account outlines) — 16px footprint
   (their full-width shapes read larger than the Channels triangle / logout at
   18px, so they sit a touch smaller). Stroke colour inherits from the nav
   .icon-btn (var(--text-meta) at rest, var(--text) on hover), same as the
   Tabler glyphs they replace. */
.nav-icon-glyph{
  width: 16px;
  height: 16px;
  display: block;
}

/* Library nav glyph: 14.4px = 10% under the 16px nav baseline.
   PROPORTION is fixed in the macro (viewBox cropped to the artwork) — that had to
   come first, because the mark filled only ~70% of its authored box and no px
   value could match the siblings while that was true. With the box cropped the
   glyph fills it, so px is now a meaningful size lever again and is the right
   place for this trim: cropped to the bounds it stands a touch taller than
   siblings like the chart mark (492x428, so ~14px of rendered height at a 16px
   box), and 10% brings it back into line. Same fractional-ratio convention as
   .saves-filter-glyph. */
.nav-library-icon{
  width: 14.4px;
  height: 14.4px;
  display: block;
}

/* Search nav glyph sits a touch larger than its 16px siblings — the magnifier's
   24-viewBox carries built-in padding (circle + handle inset), so at 16px it
   read smaller than the others; 18px balances it. */
.nav-search-icon{
  width: 18px;
  height: 18px;
  display: block;
}

.icon-img{
  object-fit: contain;
}

.icon-btn-sm{
  width: var(--icon-btn-size);
  height: var(--icon-btn-size);
  min-width: var(--icon-btn-size);
  min-height: var(--icon-btn-size);
  padding: 0;
  justify-content: center;
  align-items: center;
}

.copy-btn{
  color: var(--muted);
  font-size: var(--type-meta-size);
}

.copy-btn .icon{
  width: var(--icon-copy-glyph);
  height: var(--icon-copy-glyph);
}

.copy-btn.copied{
  /* Copy-confirmation flash takes the viewer's tier colour (not brand blue),
     matching the rest of the tier-themed detail chrome. */
  color: var(--tier-accent);
}

::placeholder{ color: var(--muted); }

small, .muted{ color: var(--muted); }

/* Transcript output, wraps, responsive, same font */
.transcript{
  font: inherit;
  font-size: 1rem;
  line-height: 1.15;

  white-space: normal;
  overflow-wrap: anywhere;
  word-break: break-word;

  width: 100%;
  max-width: 100%;

  padding: 12px 14px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: rgba(255,255,255,0.04);
}

.transcript p{
  margin: 0 0 0.9em 0;
}

.transcript p:last-child{
  margin-bottom: 0;
}

.bullet-list{
  margin: 8px 0 0 18px;
  padding: 0;
}

.bullet-list li{
  margin: 6px 0;
}

.related-link{
  display: block;
  width: calc(100% + 32px);
  margin-inline: -16px;
  padding: 12px 16px;
  border-radius: 12px;
  text-decoration: none;
  transition: background-color 120ms ease;
}

.related-list{
  width: 100%;
  margin: 0;
  padding: 0;
  gap: 0 !important;
}

.related-accordion{
  padding: 16px;
  overflow: hidden;
}

.related-accordion > summary{
  cursor: pointer;
}

.related-summary{
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 100%;
  margin: 0;
}

.related-summary-head{
  justify-content: space-between;
  align-items: center;
  flex-wrap: nowrap;
  margin: 0;
  padding-inline: 0;
}

.related-toggle{
  color: var(--text);
}

.related-toggle .icon-minus{
  display: none;
}

.related-accordion[open] .related-toggle .icon-plus{
  display: none;
}

.related-accordion[open] .related-toggle .icon-minus{
  display: block;
}

details.card.related-accordion > .related-list{
  margin-top: 0 !important;
}

.related-list .related-link + .related-link{
  margin-top: 2px;
}

.related-summary .related-link-first{
  margin-bottom: -16px;
}

details.card.related-accordion[open] .related-summary .related-link-first{
  margin-bottom: 0;
}

.related-list .related-link:last-child{
  margin-bottom: -16px;
}

.related-row{
  width: 100%;
  margin: 0;
  padding: 0;
  border-radius: 0;
}

.related-link:hover,
.related-link:focus-visible{
  background: var(--btn-hover);
  outline: none;
}

.related-row .stack{
  flex: 1 1 auto;
  min-width: 0;
}

.media-thumb{
  position: relative;
  flex: 0 0 auto;
  border-radius: 10px;
  border: 1px solid var(--border);
  overflow: hidden;
  background: var(--surface-2);
}

.library-thumb{
  width: 44px;
  min-width: 44px;
  height: 59px;
  aspect-ratio: 3 / 4;
  border: none;
  border-radius: 4px;
}

.related-thumb{
  width: 56px;
  min-width: 56px;
  height: 75px;
  aspect-ratio: 3 / 4;
}

.title-thumb{
  width: auto;
  height: calc(var(--type-display-size) * var(--type-display-line) * 3);
  max-height: 148px;
  aspect-ratio: 3 / 4;
  /* No outline border on the detail-page thumb — matches the Library /
     channel-detail thumbs (.library-thumb also strips the .media-thumb border). */
  border: none;
}

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

.media-thumb.thumb-audio img{
  width: 50%;
  height: 50%;
  object-fit: contain;
  position: absolute;
  inset: 0;
  margin: auto;
}

.media-thumb-fallback{
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--type-meta-size);
  font-weight: 600;
  letter-spacing: 0.05em;
  color: var(--muted);
  text-transform: uppercase;
}

.media-thumb:not(.thumb-missing) .media-thumb-fallback{
  opacity: 0;
}

.related-entry{
  gap: 8px;
}

.related-row strong,
.related-row .muted{
  overflow-wrap: anywhere;
}

.library-excerpt,
.related-excerpt{
  font-size: var(--type-meta-size);
  line-height: 1.12;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  line-clamp: 2;
  min-height: calc(var(--type-meta-size) * 1.12 * 2);
  max-height: calc(var(--type-meta-size) * 1.12 * 2);
}

/* Library (and channel-detail) row summary uses --text-preview — the shared
   body-copy colour (see .summary-card p below): a mid-ground that's brighter
   than the historical --muted but sits clearly under the --text titles.
   Overrides the .muted class on the element (later source order wins at equal
   specificity). The failed-state .library-failure-msg (defined later) keeps
   its own --muted — it's a diagnostic, not content. */
.library-excerpt{
  color: var(--text-preview);
}

.summary-card p,
.summary-card li{
  /* Body copy mid-ground: --text-preview is brighter than the discreet --muted
     it used to be, but sits a clear step under the --text title hierarchy (not
     full body/white). The same colour is shared across the save-detail summary,
     the library/channel-detail rows, and the dashboard Recent Saves so body
     copy reads consistently everywhere. The raw captured-evidence panel below
     uses the SAME value for one consistent brightness. */
  color: var(--text-preview);
}

.summary-card{
  /* heading→body within a single sub-block (Summary, Key Points, Core Takeaway) */
  --space-heading-body: var(--space-4);
  /* section-to-section gap between sub-blocks. Bumped from --space-4 (10px) to
     --space-6 (16px): summary callout → KEY POINTS / Core Takeaway etc. now
     reads as breathing room between distinct sections, not a continuation. */
  --space-section-block: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-section-block);
}

/* Structured-extracts container (2026-06-08): Key Points (always) + Performance
   + Top Comments (Analyst/Navigator) + Core Takeaway live INSIDE a filled radius
   card whose fill matches the dashboard cards at rest (--surface-2). The
   synthesis callout + captured-content accordion ABOVE it sit outside — they're
   their own accent-edge containers, so the detail page's summary-card itself is
   flattened (no double frame). Inner block spacing inherits the section-block
   gap from the .summary-card ancestor. */
.summary-extracts{
  display: flex;
  flex-direction: column;
  gap: var(--space-section-block, var(--space-6));
  border: 0.5px solid var(--border);
  border-radius: 12px;
  padding: 16px;
  background: var(--surface-2);
}

.summary-card .summary-block{
  display: flex;
  flex-direction: column;
  gap: var(--space-heading-body);
}

.summary-card .summary-groups{
  gap: var(--space-section-block);
}

.summary-card .section-head{
  margin: 0;
  position: relative;
  align-items: center !important;
  min-height: var(--icon-btn-size);
  padding-right: calc(var(--icon-btn-size) + var(--space-2));
}

/* Label + chevron coupling for the SUMMARY card. Mirrors .saves-heading-group
   on the library: the chevron is a label-adjacent affordance, not a
   right-aligned card-level action. Card-level actions (copy) remain
   right-aligned in .section-head. */
.summary-card .summary-heading-group{
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: nowrap;
}

.summary-card .section-head .copy-btn{
  width: var(--icon-btn-size);
  height: var(--icon-btn-size);
  min-width: var(--icon-btn-size);
  min-height: var(--icon-btn-size);
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
}

.summary-card .section-title{
  margin: 0;
}

/* Canonical reading rhythm for every save's summary card + captured accordion:
   ONE body line-height (1.2) and NO extra list-item margin, so a bullet/step gap equals a
   body-copy line gap — lists read at the same density as body copy (no drift). Applies to
   structure lists, Key Points, Performance, captured paragraphs and the summary callout. */
.summary-card p{
  margin: 0;
  font-size: var(--type-body-size);
  line-height: 1.2;
}

.summary-card .bullet-list{
  margin: 0;
  padding-left: 18px;
}

.summary-card .bullet-list li{
  margin: 0;
  font-size: var(--type-body-size);
  line-height: 1.2;
}

/* Superscript from the mathup filter (2^x -> 2<sup>x</sup>): line-height:0 keeps the raised
   exponent from inflating the line box; slightly smaller so it reads as an exponent. */
.summary-card sup{ line-height: 0; font-size: 0.72em; }

.summary-card .bullet-list .bullet-sublist{
  margin-top: 4px;
}

/* Structured takeaway ("structure") — rendered INSIDE the summary card, REPLACING the
   generic Key Points so the takeaway is shaped to the content (steps / list / recipe …).
   The header mirrors the branded PDF: the CONTENT title over a "Recipe · Food & Drink"
   (type · domain) eyebrow + PDF download; each section is a WHITE sub-header (.section-title,
   like Key Points) + body text; steps + bullets read exactly like the app's takeaway lists. */
/* --st-accent (the hybrid TYPE colour — see pill_fill) is still set inline per card but is
   currently UNUSED on the card: without a pill to give the colour meaning it read as an alarm
   on the markers, so markers are now neutral (2026-07-24). Retained as a one-rule-away lever if
   a subtle accent is wanted later; pill_fill/taxonomy_fill also feed nothing visible right now. */
.st-block{ --st-accent: var(--tier-accent, #0698ea); }
/* Structured header — CONTENT title styled like the section headers (uppercase .section-title
   treatment) over a "Recipe · Food & Drink" eyebrow in the body-text colour, PDF download pinned
   top-right. margin-bottom sets the gap to the first section. */
/* Grid so the download can't inflate the title->eyebrow gap: title (1,1) + eyebrow (2,1) stack
   tight on the left (normal 3px spacing); the download spans both rows on the right, top-aligned
   inline with the title (a flex row would size to the ~28px button and shove the eyebrow down). */
.st-head{ display: grid; grid-template-columns: 1fr auto; column-gap: 8px; margin-bottom: 14px; }
.st-title{ grid-column: 1; grid-row: 1; margin: 0; font-size: var(--type-meta-size); font-weight: 500; text-transform: uppercase; letter-spacing: 0.08em; line-height: 1.2; color: var(--text); }
.st-subtitle{ grid-column: 1; grid-row: 2; margin-top: 3px; font-size: var(--type-preview-size); line-height: 1.2; color: var(--text-preview); }
/* Generic (no-structure) block keeps the simple "KEY POINTS" label-as-header. Its
   header->content gap matches the other in-card sub-sections exactly: Performance is a bare
   section-title (margin:0) directly above its list, so .st-format carries no extra margin. */
.st-format{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 0; }
.st-block .st-format .section-title{ margin: 0; }
/* Download sits on the header row (right): canonical .icon-btn circle hover; muted at rest ->
   full-contrast icon on hover, like the chevron. */
.st-download{ grid-column: 2; grid-row: 1 / span 2; align-self: start; flex: none; color: var(--text-meta); transition: color .15s ease, background-color .15s ease; }
.st-download:hover, .st-download:focus-visible{ color: var(--text); }
.st-download-icon{ width: var(--icon-glyph-size); height: var(--icon-glyph-size); display: block; }
.st-section + .st-section{ margin-top: 13px; }
.st-block .st-section .section-title{ margin-bottom: 6px; }
.st-text{ margin: 0; font-size: var(--type-body-size); line-height: 1.2; color: var(--text-preview); }
/* Bullets + numbers are NEUTRAL — the ::marker inherits the li, so they read at the same colour
   (--text-preview), weight and size as the body copy that follows them. Without a pill to anchor
   the colour's meaning an accent marker read as an alarm; neutral markers also keep every list
   (Key Points + structure) uniform. Same 1.2 line-height, no extra item margin (matches body copy). */
.st-items{ list-style: disc; margin: 0; padding-left: 18px; }
.st-items li{ margin: 0; font-size: var(--type-body-size); line-height: 1.2; color: var(--text-preview); }
.st-steps{ list-style: decimal; margin: 0; padding-left: 20px; }
.st-steps li{ margin: 0; padding-left: 4px; font-size: var(--type-body-size); line-height: 1.2; color: var(--text-preview); }
/* Gap between the takeaway/structure block and the block that follows (Performance / Core
   Takeaway). The .summary-extracts flex container already provides the canonical
   --space-section-block (16px) gap between every block; the old extra 18px margin STACKED on
   top of that gap, making the structure->Performance gap bigger than every other inter-section
   gap. Zeroed 2026-07-27 so all extract gaps are uniform. */
.st-block + .summary-block{ margin-top: 0; }

.summary-card .tags-pills-row{
  margin-top: 0;
}

.summary-card .sites-pills-row{
  margin-top: 0;
  gap: 6px;
  row-gap: 6px;
  column-gap: 6px;
  align-items: flex-start;
}

/* Cat pills row — lives INSIDE the collapsible captured panel (first child,
   above the caption frame), so it's hidden with the panel in the default
   collapsed view and reads as the categorisation prelude to the revealed
   evidence. No margin override: the panel's natural flex gap
   (--space-section-block = 16px) provides comfortable separation from the
   caption frame below, matching the section rhythm rather than hugging it.
   (Previously a negative margin collapsed this to 6px when the row sat at the
   top of the card; inside the panel the full gap reads better.) */
.summary-card .summary-cat-pills{
  margin: 0;
  align-items: center;
}

/* Smart chevron — toggles the captured-content panel below Core Takeaway.
   Tabler ti-chevron-down at rest; aria-expanded="true" rotates it 180°
   into a chevron-up. Animated 140ms. */
.summary-chevron{
  transition: transform 140ms ease, color 140ms ease, background 140ms ease;
}
.summary-chevron .ti{
  transition: transform 140ms ease;
  font-size: 18px;
  line-height: 1;
}
.summary-chevron[aria-expanded="true"] .ti{
  transform: rotate(180deg);
}

/* Captured-content panel — hidden by default; revealed via the smart
   chevron. The [hidden] attribute drives initial state (no FOUC). Once
   the chevron opens it, the panel's [hidden] is removed and content
   animates in via opacity transition. 220ms ease matches the established
   accordion content reveal timing. */
.summary-captured-panel{
  display: flex;
  flex-direction: column;
  gap: var(--space-section-block);
  opacity: 1;
  transition: opacity 220ms ease;
}
.summary-captured-panel[hidden]{
  display: none;
}
.summary-captured-panel.is-opening{
  opacity: 0;
}

.summary-captured-section{
  display: flex;
  flex-direction: column;
  gap: var(--space-heading-body);
}

/* Inside the captured-content panel, .subtitle elements adopt caps
   section-label treatment so they share parallel structure with KEY POINTS
   / CORE TAKEAWAY in the same card. This overrides the global .subtitle
   sentence-case rule for this surface only.
   Colour: var(--text) (bright) — these labels point at visible content
   the user is actively reading inside the SUMMARY card. The bright
   treatment unifies them with SUMMARY / KEY POINTS / CORE TAKEAWAY.
   Rule (banked): within a single card containing multiple sub-sections,
   all section labels are caps + bright regardless of whether they'd
   otherwise be classified as content-area labels. Caps + bright unifies
   the in-card hierarchy. */
.summary-captured-section .subtitle{
  font-size: var(--type-label-size);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text);
  margin: 0;
}

/* SUMMARY card section labels (KEY POINTS, CORE TAKEAWAY) — explicitly
   bright. The global .section-title rule already uses var(--text), but
   re-declared here so any future cascade pressure or rule consolidation
   doesn't silently dim the labels for visible content. */
.summary-card .section-title{
  color: var(--text);
}

/* SOURCE card heading — meta-dim, intentionally demoted. The SOURCE
   card is a diagnostic footer block ("show your work" scaffolding);
   its label sits lower in the visual hierarchy than the visible-content
   labels in the SUMMARY card. Same caps + 0.08em + size as other card
   headings; only the colour differs.
   Rule (banked): visible content section labels are bright; diagnostic
   / demoted section labels are meta-dim. Caps + letter-spacing + size
   are uniform across both variants — only the colour token differs by
   visibility / hierarchy. */
.source-card .card-heading{
  color: var(--text-meta);
}

/* Detail-page heading hierarchy: SUMMARY is the ONE prominent card title; every OTHER
   card heading (SOURCE, RELATED SAVES, …) is a section label matching the KEY POINTS /
   PERFORMANCE sub-headings (11px, --type-meta-size, weight 500). */
.item-detail-page .card-heading{
  font-size: var(--type-meta-size);
  font-weight: 500;
  letter-spacing: 0.08em;
  line-height: 1.15;
}
.item-detail-page .summary-card .card-heading{
  font-size: var(--type-card-title-size);
  font-weight: 400;
  letter-spacing: var(--type-card-title-track);
  line-height: var(--type-card-title-line);
}

.summary-captured-section p,
.summary-captured-section li{
  /* Matched to the summary body (--text-preview) for a consistent reading
     brightness across synthesised output and raw captured content, and the
     universal 1.2 body line-height (paragraphs keep an 8px inter-block gap). */
  color: var(--text-preview);
  font-size: var(--type-body-size);
  line-height: 1.2;
  margin: 0 0 8px;
}

.summary-captured-section p:last-child,
.summary-captured-section li:last-child{
  margin-bottom: 0;
}

.summary-captured-section .bullet-list{
  margin: 0;
  padding-left: 18px;
}

/* SOURCE card — diagnostic footer. Slightly tightened padding to
   signal hierarchy (SOURCE sits below SUMMARY, less prominent). */
.source-card{
  padding: 12px 16px;
}

/* Captured-content nested container — asymmetric-accent-edge frame
   inside the SUMMARY card. Communicates "raw captured artefacts" as
   distinct from the synthesised summary above. Neutral grey stripe
   (var(--stripe-rest)) — not a status colour, not a personalisation
   colour. The "supplementary captured material" semantic.
   No background tint — the stripe + hairline border alone are
   sufficient differentiation; tinting at neutral grey is too washed
   out in dark mode to register. */
.summary-captured-frame{
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-section-block);
  padding: 12px 14px 12px 11px;
  border-left: 3px solid var(--stripe-rest);
  border-radius: 0 10px 10px 0;
  /* Filled to match the structured-extracts container + dashboard cards at rest
     (2026-06-08) — the expanded captured content (Caption / Transcription /
     Extracted Text …) reads as a contained surface, not floating text. */
  background: var(--surface-2);
}
.summary-captured-frame::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
}

/* Navigator-only Display Preferences toggle — single-row toggle with a
   compact switch on the right. Inherits .account-pref-row stripe
   geometry; the switch is a presentational indicator only (form submit
   carries the flipped value). */
.account-display-pref-row{
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.account-pref-switch{
  width: 32px;
  height: 18px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 0.5px solid var(--border);
  position: relative;
  flex-shrink: 0;
  transition: background 140ms ease, border-color 140ms ease;
}
.account-pref-switch-thumb{
  position: absolute;
  top: 1.5px;
  left: 1.5px;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: var(--text-meta);
  transition: left 140ms ease, background 140ms ease;
}
/* On-state switch (any pref row — Filter + Display) in the user's tier colour,
   not brand blue (2026-06-08). */
.account-pref-row.is-active .account-pref-switch{
  background: color-mix(in srgb, var(--tier-accent) 20%, transparent);
  border-color: var(--tier-accent);
}
.account-pref-row.is-active .account-pref-switch-thumb{
  left: 16.5px;
  background: var(--tier-accent);
}

.summary-callout {
  background: var(--callout-bg);
  border-radius: 8px;
  padding: 14px 16px;
}
/* A long summary is split into >=2 paragraphs (summary_paras); give consecutive paragraphs
   a real break (the canonical .summary-card p is margin:0, which would otherwise butt them). */
.summary-callout p + p{ margin-top: 0.75em; }

.item-detail-page .summary-callout {
  /* Tier-coloured stripe — always-on personalisation. The body's tier-{tier}
     class sets --tier-accent and --tier-accent-rgb; the var() fallback chain
     resolves to the signature blue callout-accent (and its rgb triplet) for
     surfaces that don't carry a tier class. */
  border-left: 3px solid var(--tier-accent, var(--callout-accent));
  background: rgba(var(--tier-accent-rgb, 33, 150, 243), 0.06);
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  border-top-right-radius: 8px;
  border-bottom-right-radius: 8px;
}

.summary-callout--processing {
  background: var(--callout-bg-processing);
}

/* Caption-anchored fallback provenance: a QUIET note in the SOURCE footer, not a warning above
   the Card. The Card produced a result (thin but real), so this just explains it was built from
   the caption — meta-dim register, matching the rest of the SOURCE diagnostic block. */
.source-caption-note {
  margin: 2px 0 0;
  color: var(--text-meta);
  font-size: var(--type-preview-size);
  line-height: 1.4;
}

/* Scrollbar for transcript if too long */
.transcript{
  max-height: 400px;
  overflow-y: auto;
}
.duplicate-card{
  padding: 20px;
}

.modal-overlay{
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 999;
  /* Scroll-safe centring: a modal taller than the viewport (e.g. a landscape
     phone) must scroll instead of clipping its top/bottom off-screen. overflow
     here + margin:auto on the card (below) centres when it fits and scrolls from
     the top when it doesn't — the canonical flexbox pattern. Padding keeps the
     card off the edges while scrolling. */
  overflow-y: auto;
  padding: 16px;
  /* Trap scroll inside the overlay so scrolling the modal (or over-scrolling a
     short one) never chains through to the background app. */
  overscroll-behavior: contain;
}
/* `hidden` must beat display:flex (author rule beats the UA [hidden] rule) —
   else a modal toggled via the hidden attribute (the About modal) renders open
   and stuck. Mirrors the .more-menu[hidden] guard. */
.modal-overlay[hidden]{
  display: none;
}

.modal-card{
  width: min(520px, 90vw);
  /* margin:auto centres the card when it fits and lets it scroll from the top of
     the overlay when it's taller than the viewport (pairs with the overlay's
     overflow-y:auto). */
  margin: auto;
  padding: 20px;
  border-radius: 16px;
  border: 1px solid var(--border);
  background: var(--surface-chrome);
  box-shadow: var(--shadow-lg);
}

.modal-card > strong{
  font-size: var(--type-body-size);
  font-weight: 500;
  color: var(--text);
}

.modal-card .muted{
  font-size: var(--type-body-size);
  line-height: 1.5;
  color: var(--text-meta);
}

/* Transcript download dialogue — format checkbox list + action row. Reuses
   the .confirm-modal-checkbox-row vocabulary for each format row. */
.transcript-format-list{
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.transcript-download-actions{
  gap: 10px;
  justify-content: flex-end;
  margin-top: 12px;
}
.transcript-download-actions #transcript-download-submit{
  margin-top: 0;
}
.transcript-download-actions #transcript-download-submit:disabled{
  opacity: 0.5;
  cursor: not-allowed;
}

/* Toast region + toast primitive.
   Fixed bottom-right at desktop; full-width pinned to viewport edges at
   ≤560px. Z-index 900: above page chrome, BELOW the 999 modal overlay so
   an open modal is never obscured by a queued toast. column-reverse layout
   stacks new toasts at the bottom of the visible region (closest to where
   the eye naturally lands after a confirm). Region itself is pointer-
   events: none so it doesn't block clicks on page content behind it; each
   toast re-enables pointer-events: auto. */
.toast-region{
  position: fixed;
  bottom: 16px;
  right: 16px;
  z-index: 900;
  display: flex;
  flex-direction: column-reverse;
  gap: 8px;
  pointer-events: none;
  max-width: 420px;
}
.toast{
  pointer-events: auto;
  display: inline-flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  background: var(--surface-chrome);
  border: 0.5px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-sm);
  font-size: var(--type-meta-size);
  color: var(--text);
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 220ms ease, transform 220ms ease;
}
.toast.toast--visible{
  opacity: 1;
  transform: translateY(0);
}
.toast--info .toast-icon{
  color: var(--text-meta);
  font-size: 16px;
  line-height: 1;
}
/* Brand info SVG inside the toast icon (replaces the Tabler glyph). Sized to
   the prior 16px glyph; colour comes from the variant rule via currentColor. */
.toast-icon svg{
  width: 16px;
  height: 16px;
  display: block;
}
/* Success variant — green icon for positive-outcome toasts (e.g. admin
   tier-switch confirmed, future "Saved", "Synced", etc.). Matches
   .toast--info's icon-only colouring pattern; toast background stays
   --surface-chrome neutral. Colour sources from the project's canonical
   success pair (--pill-done-fg, also used by status pills + the new-save
   page's connected-Instagram traffic light). Banked: tier-switch admin
   bypass Commit 3 of 3 (2026-05-27). */
.toast--success .toast-icon{
  color: var(--pill-done-fg);
  font-size: 16px;
  line-height: 1;
}
.toast-message{
  flex: 1;
  min-width: 0;
  color: var(--text);
}
.toast-dismiss{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border: 0;
  background: transparent;
  color: var(--text-meta);
  cursor: pointer;
  border-radius: 4px;
  padding: 0;
  transition: color 140ms ease, background 140ms ease;
}
.toast-dismiss .ti{
  font-size: 14px;
  line-height: 1;
}
.toast-dismiss:hover,
.toast-dismiss:focus-visible{
  color: var(--text);
  background: var(--btn-hover);
  outline: none;
}
@media (max-width: 560px){
  /* Mobile: pin the toast region to viewport edges (not the right edge
     only) so toasts span full width. Banked pattern — toast region is
     X-axis-only on mobile per the global mobile-rule (no Y-axis overrides). */
  .toast-region{
    left: 16px;
    right: 16px;
    max-width: none;
  }
}
@media (prefers-reduced-motion: reduce){
  .toast{
    transition: opacity 0ms;
    transform: none;
  }
}

.library-link{
  display: block;
  width: 100%;
}

.library-shell{
  padding: 16px;
  overflow: hidden;
}

/* Library section grouping (2026-06-21) — the "Library" page title is retired;
   Pinned + temporal sections (Today / This week / Earlier this month / Older)
   are the page's main headings. Each section title is a full .card-heading (the
   size the old Library title used), so a per-section chevron sits beside it
   cleanly. The outer wrapper stacks [header + its own grid] per section; a
   larger inter-SECTION gap separates groups while each grid keeps its own 6px
   inter-row gap. */
.library-sections{
  display: flex;
  flex-direction: column;
  gap: 20px;
  width: 100%;
}
.library-section{
  display: flex;
  flex-direction: column;
  /* Heading→first-row buffer matches the Library/dashboard 8px rhythm canon. */
  gap: 8px;
}
/* Section heading row — same shape as the retired Library heading row (title +
   optional controls left, density toggle right). The .density-btn min-height
   keeps the row at the canonical 40px so the title baseline matches the old
   Library title. The empty-state variant carries only the density toggle. */
.library-section-head{
  width: 100%;
  /* Constant height whatever controls the section carries. The icon buttons are
     --icon-btn-size tall, so a head WITHOUT them used to collapse to the title's
     line-box — which meant 'Older' sat higher than its siblings, and anything
     that added or removed a control shifted every following block (the calendar
     visibly jumped). Reserving the row makes the rhythm identical across
     sections and immune to control changes. */
  min-height: var(--icon-btn-size);
  display: flex;
  align-items: center;
}
.library-section-title{
  /* Inherits the .card-heading face (uppercase, --type-card-title-size). */
  margin: 0;
}

.library-list{
  width: 100%;
  margin: 0;
  padding: 0;
  gap: 6px;
}

/* The Library's own list (--main) renders as a 2-column grid on wide viewports,
   mirroring the dashboard Recent Saves. align-items: start keeps each card at
   its natural content height (no stretch — important so the compact/rich
   absolute action-row overlay still bottom-aligns within each card). Channel
   detail keeps a single column (it has no --main). Collapses to 1 column at the
   same 720px breakpoint the dashboard grids use. */
.library-list.library-list--main{
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: start;
}
@media (max-width: 720px){
  .library-list.library-list--main{
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ── density variants ──────────────────────────────────────── */

/* thumbnail + side column — unified 44×59 (3:4) across all density modes; see base rules */

/* Save detail page eyebrow — platform · creator handle · date above the title.
   Distinct typography from the library row eyebrow (11px sentence case here vs
   10px uppercase in rows). Creator handle is a meta-dim → bright-text hover
   link, no underline at rest or on hover (matches account-card-action pattern). */
.detail-eyebrow{
  display: flex;
  gap: 4px;
  align-items: center;
  flex-wrap: wrap;
  font-size: var(--type-meta-size);
  font-weight: 400;
  color: var(--text-meta);
  margin-bottom: 4px;
}

.detail-eyebrow-handle{
  color: var(--text-meta);
  text-decoration: none;
  transition: color 140ms ease;
}

.detail-eyebrow-handle:hover,
.detail-eyebrow-handle:focus-visible{
  color: var(--text);
  text-decoration: none;
}

/* Platform glyph in the detail eyebrow — replaces the old platform word.
   Same flex/line-height-0 treatment as the Library row eyebrow so the inline
   SVG sits centred on the meta-text baseline. */
.detail-eyebrow-platform{
  display: flex;
  align-items: center;
  flex-shrink: 0;
  line-height: 0;
}

/* eyebrow */
.library-eyebrow{
  display: flex;
  gap: 4px;
  align-items: center;
  font-size: var(--type-label-size);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-meta);
}
.library-eyebrow-platform{
  display: flex;
  align-items: center;
  flex-shrink: 0;
  line-height: 0;
}
.library-list--compact .library-eyebrow{ display: none; }

.library-list--compact .library-excerpt{
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  opacity: 1;
  min-height: 0;
  max-height: calc(0.84rem * 1.12 * 2);
}

.library-list--expanded .library-excerpt{
  /* Rich density caps the summary at 6 lines at REST too, so the row height is
     fixed. The action row's space below is permanently reserved (see the
     hover-reveal block), so hovering fades the icons in WITHOUT growing the
     listing — cat pills stay pixel-stable. */
  display: -webkit-box;
  -webkit-line-clamp: 6;
  line-clamp: 6;
  -webkit-box-orient: vertical;
  overflow: hidden;
  max-height: calc(var(--type-meta-size) * 1.12 * 6);
  min-height: 0;
}

/* title: single-line ellipsis in Compact; slight size bump in Rich */
.library-list--compact .library-title{
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  display: block;
}
/* tag row beneath summary */
.library-tag-row{
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: center;
}
.library-list--compact .library-tag-row{ display: none; }
.library-list--expanded .library-tag-row{ margin-top: 4px; }

/* Archive + Trash + compact status pill — bottom of text column */
.library-bottom-actions{
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 4px;
  margin-top: 4px;
  --icon-btn-size: 28px;
  --icon-glyph-size: 15px;
}

/* Compact hover-reveal: at rest the row shows 2 summary lines and NO action
   icons (reduces visual noise on first inspection). On hover/focus the summary
   clips to its FIRST line (the 2nd line gives way) and the action row fades in
   BENEATH it — so at least one line of body copy stays visible and no text
   bleeds behind the icons (the row backgrounds are translucent, so an opaque
   mask isn't available; clipping the lower line is the theme-agnostic
   equivalent). The text column is min-height: 59px (the thumb height) so the
   absolutely-positioned action row always bottom-aligns to the thumbnail — it
   never floats up to collide with the title when the summary is short or
   absent (e.g. failed saves). Compact only — Expanded keeps the actions in
   flow, always shown. */
.library-list--compact .library-link{
  position: relative;
  min-height: 59px;
}
/* Reveal trigger: :hover (desktop) OR .actions-revealed (a class JS adds on the
   first tap of a row on TOUCH devices, which have no hover — see base.html). */
.library-list--compact .library-row-shell:hover .library-excerpt,
.library-list--compact .library-row-shell:focus-within .library-excerpt,
.library-list--compact .library-row-shell.actions-revealed .library-excerpt{
  /* Hard-clamp to exactly ONE line on hover (not a pixel max-height, which can
     leave a sliver of the 2nd line) so the row reads cleanly under the icons. */
  -webkit-line-clamp: 1;
  line-clamp: 1;
  max-height: calc(0.84rem * 1.12);
}
.library-list--compact .library-bottom-actions{
  position: absolute;
  left: 0;
  bottom: 0;
  margin-top: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-action-fade, 140ms) ease;
}
.library-list--compact .library-row-shell:hover .library-bottom-actions,
.library-list--compact .library-row-shell:focus-within .library-bottom-actions,
.library-list--compact .library-row-shell.actions-revealed .library-bottom-actions{
  opacity: 1;
  pointer-events: auto;
}
/* In-flight saves (compact): pill visible at rest, action buttons hidden until
   the save completes — see the .recent-save-row.is-processing note above. */
.library-list--compact .library-row-shell.is-processing .library-bottom-actions{
  opacity: 1;
}
.library-list--compact .library-row-shell.is-processing .library-bottom-actions > :not(.status-pill){
  display: none;
}

/* Rich (Expanded) hover-reveal — the action row is hidden at rest and fades in
   on hover. This shared rule is the IN-FLOW reserved-strip variant used by
   CHANNEL DETAIL, which keeps its cat-pill row: the action row sits in flow
   below the cat pills and its space is permanently reserved (base margin-top +
   height), so revealing the icons never changes the row height. The Library
   (.library-list--main) overrides this to an absolute overlay below — it has no
   cat-pill row, so it can unify onto the Compact mechanism. */
.library-list--expanded .library-bottom-actions{
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-action-fade, 140ms) ease;
}
.library-list--expanded .library-row-shell:hover .library-bottom-actions,
.library-list--expanded .library-row-shell:focus-within .library-bottom-actions,
.library-list--expanded .library-row-shell.actions-revealed .library-bottom-actions{
  opacity: 1;
  pointer-events: auto;
}
/* In-flight saves (expanded): pill visible at rest, action buttons hidden until
   the save completes — see the .recent-save-row.is-processing note above. */
.library-list--expanded .library-row-shell.is-processing .library-bottom-actions{
  opacity: 1;
}
.library-list--expanded .library-row-shell.is-processing .library-bottom-actions > :not(.status-pill){
  display: none;
}

/* Library rich rows (.library-list--main) carry NO cat-pill row. The action row
   is IN FLOW below the 6-line summary and its space is PERMANENTLY RESERVED
   (always laid out, just opacity 0 at rest → 1 on hover/tap). So revealing the
   icons is a pure cross-fade: the card height NEVER changes (nothing below it
   moves — important in the 2-column grid). We don't overlay + clip the summary
   to hide the gap, because -webkit-line-clamp won't truncate inside a box
   taller than the clamp (the reserve), so long bodies bled under the icons.
   Trade-off vs Recent Saves (which overlays): a small reserved strip sits below
   the summary at rest. Robust at every width AND on touch. */
.library-list--main.library-list--expanded .library-row-shell{
  /* Match Compact's 8px top padding so the content top aligns with Compact;
     the extra rich height grows downward. */
  padding-top: 8px;
}
.library-list--main.library-list--expanded .library-bottom-actions{
  position: static;
}
/* 2-column rich grid alignment: give every card a UNIFORM height so the two
   columns line up (no ragged grid). Fix the title to 2 lines and the summary to
   6 lines — both with min-height = max so short content reserves the space and
   long content clamps to it — and the reserved action strip is constant, so all
   cards are identical height. Scoped to the 2-column breakpoint (min-width:
   721px); the single-column / mobile layout keeps natural heights (alignment
   only matters side-by-side). */
@media (min-width: 721px){
  .library-list--main.library-list--expanded .library-title{
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    line-height: 1.25;
    min-height: calc(var(--type-preview-size) * 1.25 * 2);
  }
  .library-list--main.library-list--expanded .library-excerpt{
    min-height: calc(var(--type-meta-size) * 1.12 * 6);
  }
}

.library-title{
  margin: 0;
  font-size: var(--type-preview-size);
  font-weight: 500;
}
/* ──────────────────────────────────────────────────────────── */

.library-row-wrap{
  position: relative;
  width: 100%;
}

.library-row{
  align-items: flex-start;
  flex-wrap: nowrap;
}

.library-row-shell{
  align-items: flex-start;
  flex-wrap: nowrap;
  /* Top padding bumped 8 -> 12 to equalise perceived top/bottom whitespace.
     Geometric cause of the asymmetry: align-items: flex-start above + stacked
     vertical content (thumbnail flush to top, action row at bottom carrying
     icon-button line-height padding). The 4px extra at the top compensates
     for the action-row's intrinsic bottom airspace. Independent of the
     border-left stripe — sibling surfaces .recent-save-row and
     .trending-accent-row use align-items: center and stay 8px symmetric. */
  padding: 12px 16px 8px 13px;
  border-left: 3px solid var(--stripe-rest);
  border-radius: 0 10px 10px 0;
  position: relative;
  background: var(--card-rest);
  transition: background 150ms ease, border-left-color 150ms ease;
}
/* Compact rows: symmetric top/bottom padding (8/8), matching the Recent Saves
   row feel. The base rule above bumps the top to 12px to offset the IN-FLOW
   action row's bottom airspace — but in compact the action row is absolute
   (out of flow), so that compensation no longer applies and the extra top
   padding just made the gap above the thumb larger than below it. */
.library-list--compact .library-row-shell{
  padding-top: 8px;
}
.library-row-shell::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
}

.library-row-shell:hover,
.library-row-shell:focus-within{
  background: var(--card-hover);
  border-left-color: var(--tier-accent);
}

.library-row-shell.is-locked{
  background: var(--surface-2);
  opacity: 0.56;
  border-left-color: transparent;
}

.library-row-shell.is-locked:hover,
.library-row-shell.is-locked:focus-within{
  background: var(--surface-2);
  border-left-color: transparent;
}

.library-row-shell.is-locked .media-thumb img{
  filter: grayscale(1) saturate(0.2);
  opacity: 0.82;
}

.library-row-shell.is-locked .media-thumb{
  border-color: var(--border);
}

.library-row-shell.is-locked .library-title,
.library-row-shell.is-locked .library-excerpt,
.library-row-shell.is-locked .status-line{
  color: var(--muted);
}

.library-row-shell.is-locked .status-pill{
  background: rgba(107, 125, 146, 0.10);
  color: var(--text-meta);
}

.library-row-shell.is-locked .status-pill .status-dot{
  background: var(--text-meta);
}

@keyframes status-spin{
  to{ transform: rotate(360deg); }
}
.status-processing .ti{
  animation: status-spin 1.2s linear infinite;
  display: inline-block;
}

/* Detail action row in-flight indicator: a bare amber rotating loader, sized
   to the icon-btn footprint so it aligns with the sibling action icons. No
   pill background and no label — the descriptive text lives in the amber
   processing card below. (Retains .status-processing for the spin animation.) */
.status-spinner-only{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--icon-btn-size);
  height: var(--icon-btn-size);
  color: var(--pill-processing-fg);
}
.status-spinner-only .ti{
  font-size: 16px;
  line-height: 1;
}

.library-row-shell.is-locked .progress-fill{
  background: var(--border-2);
}

.library-row-shell.is-locked .icon-btn{
  color: var(--muted);
  border-color: var(--border);
}

/* Locked rows are paywalled (saves above the downgraded tier's cap) — their
   only affordance is Upgrade. Suppress the hover-reveal action cluster
   entirely: in compact it is position:absolute at the row bottom and fades in
   on hover, so on a locked row the pin / channels / delete icons were fading in
   directly over the "Locked · Upgrade" pills (and the summary). A locked save
   isn't pin/channel/delete-able through the row, so the cluster has no business
   appearing. Killing it removes the overlay at every breakpoint + on touch. */
.library-row-shell.is-locked .library-bottom-actions{
  display: none;
}

.library-row-shell.is-locked .tag-link{
  color: var(--accent-blue);
  border-color: var(--accent-blue-soft);
  opacity: 1;
}

.library-side{
  position: relative;        /* anchors .library-thumb-play absolute overlay */
  width: 44px;
  min-width: 44px;
  align-items: center;
}

/* Play triangle overlay — tier-coloured backdrop, hover-only */
.library-thumb-play{
  position: absolute;
  top: 0;
  left: 0;
  width: 44px;
  height: 59px;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--motion-action-fade) ease;
  z-index: 1;
}

.library-thumb-play-btn{
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: auto;
  text-decoration: none;
  line-height: 1;
}

.library-thumb-play-btn .play-triangle{
  width: 17px;
  height: auto;
  fill: #fff;
  filter: drop-shadow(0 1px 3px rgba(0,0,0,0.5));
}

.library-row-shell:hover .library-thumb-play,
.library-row-shell:focus-within .library-thumb-play{
  opacity: 1;
}

.library-row-shell.is-locked .library-thumb-play{
  display: none;
}

/* Detail-page thumbnail: persistent (always-visible) play triangle overlay.
   Departure from row-level pattern — single-item view, no row content to
   compete with, so the affordance is unambiguous at rest. */
.title-thumb-play{
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}

.title-thumb-play .play-triangle{
  width: 17px;
  height: auto;
  fill: #fff;
  filter: drop-shadow(0 1px 3px rgba(0,0,0,0.5));
}

.library-link-row{
  display: flex;
  align-items: flex-start;
  gap: 6px;
  width: 100%;
  margin: 0;
  padding: 0;
  border-radius: 0;
  transition: none;
}

.library-link-row:hover,
.library-link-row:focus-visible{
  background: transparent;
  outline: none;
}

.library-link-row.is-locked{
  cursor: default;
}

.library-content-link{
  text-decoration: none;
  color: inherit;
  display: block;
  flex: 1 1 auto;
  min-width: 0;
}

.library-thumb-locked{
  pointer-events: none;
}

/* ------------------------------------------------------------------
   Account page (/ui/upgrade) — stripe-row vocabulary
   Plan rows, detail rows, and pref rows all share the same asymmetric
   accent edge pattern as .library-row-shell (3px stripe + 3-sided
   0.5px hairline via ::after, border-radius 0 10px 10px 0).
   ------------------------------------------------------------------ */
.account-row-list{
  width: 100%;
}

.account-plan-row,
.account-pref-row,
.account-detail-row{
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  /* Uniform settings-row height — matches the IG connected container (62.4px) so
     every accent-edge settings container reads at one height; the Plan tier rows
     (.account-plan-row, excluded here) stay taller for visual weight. */
  min-height: 62.4px;
  padding: 10px 14px 10px 11px;
  border: 0;
  border-left: 3px solid var(--stripe-rest);
  border-radius: 0 10px 10px 0;
  background: var(--card-rest);
  text-align: left;
  position: relative;
  color: inherit;
  font: inherit;
  transition: background 140ms ease, border-left-color 140ms ease;
}
.account-plan-row::after,
.account-pref-row::after,
.account-detail-row::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
}

/* Interactive rows (plans, prefs) hover-lighten */
.account-plan-row,
.account-pref-row{
  cursor: pointer;
}
.account-plan-row:hover,
.account-plan-row:focus-visible,
.account-pref-row:hover,
.account-pref-row:focus-visible{
  background: var(--card-hover);
  outline: none;
}

/* Plan rows — per-tier stripe colour on hover and active */
.account-plan-row.tier-curator{
  --tier-accent: #A78BFA;
  --tier-accent-rgb: 167, 139, 250;
}
.account-plan-row.tier-analyst{
  --tier-accent: #F472B6;
  --tier-accent-rgb: 244, 114, 182;
}
.account-plan-row.tier-navigator{
  --tier-accent: #4F46E5;
  --tier-accent-rgb: 79, 70, 229;
}
/* Accent edge state machine (canonical, 2026-06-03):
     - current tier (.is-active):                       tier accent, always
     - non-current EXPANDED ([data-...-expanded="true"]): tier accent
     - non-current collapsed:                            muted (--stripe-rest)
   Hover does NOT drive the accent — at any moment the only accented rows
   are the current tier and whichever row is expanded. Dropping :hover also
   fixes mobile sticky-:hover keeping the accent after a row collapses. The
   background hover-lighten (above) is unaffected. */
.account-plan-row.is-active,
.account-plan-row-container[data-tier-accordion-expanded="true"] .account-plan-row{
  border-left-color: var(--tier-accent);
}

/* Expanded tier card: the tier label (ANALYST / CURATOR / NAVIGATOR) takes the
   tier accent colour — reinforces which tier the user has opened. --tier-accent
   cascades from the .account-plan-row.tier-{tier} ancestor. */
.account-plan-row-container[data-tier-accordion-expanded="true"] .account-plan-name{
  color: var(--tier-accent);
}

.account-plan-icon{
  width: 28px;
  height: 28px;
  min-width: 28px;
  border-radius: 50%;
  background: rgba(var(--tier-accent-rgb), 0.15);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--tier-accent);
  flex-shrink: 0;
}
.account-plan-icon i{
  font-size: 14px;
}
.account-plan-body{
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1;
}
/* Tier name — small caps label, supporting scaffolding (not the headline). */
.account-plan-name{
  font-size: var(--type-label-size);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-meta);
}
/* Verb tagline — the heart of each card. The whole phrase reads in ONE colour
   (--text-meta) now: the original bright-verb / muted-rest colour split read as
   distracting. The verb keeps a slightly heavier weight (500) for a subtle
   emphasis, but no longer changes colour. The .tier-tagline-verb /
   .tier-tagline-rest spans still own their treatment; the wrapper sets the
   type vocabulary. */
.account-plan-tagline{
  font-size: var(--type-body-size);
  font-weight: 400;
  line-height: 1.2;
  margin-top: 2px;
}
.tier-tagline-verb{
  color: var(--text-meta);
  font-weight: 500;
}
.tier-tagline-rest{
  color: var(--text-meta);
}
/* Price line (or trial framing) — supporting meta. */
.account-plan-price{
  font-size: var(--type-meta-size);
  font-weight: 400;
  color: var(--text-meta);
  line-height: 1.2;
}

/* Current tier card (Fix 3, this session) — name brightens to tier-accent
   to mark identity. Inert: rendered as <div role="presentation"> not
   <button>, so cursor stays default (not the .account-plan-row pointer).
   The .is-active stripe colour rule above already handles the stripe;
   this completes the colour distinction (stripe + icon + name = full
   tier-accent treatment) versus non-current rows (grey at rest, accent
   only on hover). Rules sit AFTER the base .account-plan-name rule so
   the cascade order matches semantic order (base → modifier override). */
.account-plan-row.is-active{
  cursor: default;
}
.account-plan-row.is-active .account-plan-name{
  color: var(--tier-accent);
}

/* Non-current tier cards — icon backplate is muted grey at rest. The
   stripe is already grey via the base --stripe-rest rule; only the icon
   needs the override. 140ms ease on background + color for a calm reveal —
   same timing as the stripe transition. Uses the canonical --pill-queued-bg
   / --text-meta tokens (theme-aware) rather than a new rgba literal. The
   .is-active and .account-plan-row-trial-expired-link variants are
   excluded: both already render in their accented states (current tier =
   tier-accent; trial-expired Curator = anchor with its own colour). */
.account-plan-row:not(.is-active):not(.account-plan-row-trial-expired-link) .account-plan-icon{
  background: var(--pill-queued-bg);
  color: var(--text-meta);
  transition: background 140ms ease, color 140ms ease;
}
/* Icon backplate follows the same accent state machine as the stripe:
   tier-accent only when the row is EXPANDED (non-current) — NOT on hover.
   Keeps the expanded row's icon + stripe visually in lockstep, and avoids
   the mobile sticky-:hover bug that kept the icon accented after collapse. */
.account-plan-row-container[data-tier-accordion-expanded="true"] .account-plan-row:not(.is-active):not(.account-plan-row-trial-expired-link) .account-plan-icon{
  background: rgba(var(--tier-accent-rgb), 0.15);
  color: var(--tier-accent);
}

/* Tier identity line inside MY PLAN card (Fix 2, this session).
   Folded in from the retired tier strip on /ui/upgrade — "You're a/an
   <Tier> · <Verb> <rest>". Mirrors the .tier-strip-title + .tier-strip-sub
   emphasis pattern exactly. Tier name resolves to var(--tier-accent) via
   the body's tier-{tier} class cascade; verb is bright (matches the row's
   .tier-tagline-verb token); rest is meta-dim. Single line, sized to
   body type (13px). Sits between the MY PLAN heading and the progress
   bar. */
.account-plan-identity{
  font-size: var(--type-body-size);
  font-weight: 400;
  line-height: 1.3;
  color: var(--text);
}
.account-plan-identity-intro{
  color: var(--text);
}
.account-plan-identity-tier{
  color: var(--tier-accent);
  font-weight: 500;
}
.account-plan-identity-sep{
  color: var(--text-meta);
}
/* "Act on your discoveries" (verb + trailing fragment) reads in the muted body
   colour, matching the tier card's second line on the other surfaces
   (2026-06-08). The "You're a" intro + tier name stay bright/accented. */
.account-plan-identity-verb{
  color: var(--muted);
  font-weight: 500;
}
.account-plan-identity-rest{
  color: var(--muted);
}

/* Trial-expired Curator card — price line is now a compound affordance:
   amber "Trial expired" + meta-dim "View your interest map →" CTA. Per-child
   colours live on the .trial-expired-amber / .trial-expired-sep /
   .trial-expired-cta classes at the end of this stylesheet (next to the
   /ui/trial-expired page rules). The wrapper span carries no colour of its
   own so child spans win deterministically. */

/* Post-trial Analyst recommendation — subtle elevated treatment when the
   Curator user's trial has expired. The stripe is tinted at rest to draw
   the eye without becoming pushy: 35% of var(--tier-accent) alpha on the
   stripe colour. Hover / is-active states still win. */
.account-plan-row-recommended:not(.is-active):not(:hover){
  border-left-color: rgba(var(--tier-accent-rgb), 0.35);
}

/* Tier card accordion (replaces the floating tooltip popover, 2026-06-03).
   Each tier row is a collapsible accordion. The header (the existing
   .account-plan-row — icon + name + tagline + price) is the whole-row
   click affordance; a chevron <button> on its right carries the canonical
   ARIA (aria-expanded + aria-controls), mirroring the Library saves-filter
   split hit-target pattern. The body reveals the comparison payload (price
   · saves · channels + features) that the tooltip used to carry, plus an
   in-body switch CTA on non-current tiers. One open at a time, JS in
   base.html.

   .account-plan-row-container is kept (was the tooltip's relative wrapper);
   it now anchors the accordion's per-row open state. */
.account-plan-row-container{
  position: relative;
}
.tier-accordion-header{
  cursor: pointer;
}
/* Chevron sits at the row's right edge (.account-plan-body has flex:1 and
   pushes it there). Canonical content-reveal vocabulary: ti-chevron-down
   rotates 180° on aria-expanded="true". 140ms matches .summary-chevron. */
.tier-accordion-chevron{
  flex: 0 0 auto;
  transition: transform 140ms ease, color 140ms ease, background 140ms ease;
}
.tier-accordion-chevron .ti{
  transition: transform 140ms ease;
  font-size: 18px;
  line-height: 1;
}
.tier-accordion-chevron[aria-expanded="true"] .ti{
  transform: rotate(180deg);
}
/* Touch devices emulate :hover on tap and hold it sticky until the user
   taps elsewhere — which left two surfaces highlighted after a row
   collapsed: the chevron's .icon-btn background circle, and the row
   header's hover-lighten background. Suppress both on no-hover (touch)
   devices; desktop (hover: hover) keeps the normal hover feedback. Same
   mobile-sticky-:hover class of bug as the accent edge fix.
     - chevron: reverts to its transparent rest bg (wins over the global
       .icon-btn:hover rule by later source order at equal specificity).
     - row header: reverts to --card-rest (the .account-plan-row rest bg);
       the compound selector outranks the shared .account-plan-row:hover
       rule on specificity. */
@media (hover: none){
  .tier-accordion-chevron:hover{
    background: transparent;
  }
  .account-plan-row.tier-accordion-header:hover{
    background: var(--card-rest);
  }
  /* The CTA's hover-brighten must not stick after a tap opens the confirm
     modal; :active still supplies the brighten feedback during the press. */
  .tier-accordion-cta:hover{
    filter: none;
  }
}
/* Body panel — hidden by default via [hidden] (no FOUC). 220ms reveal
   matches the established accordion content-reveal timing. */
.tier-accordion-body{
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: 10px 12px 12px;
  opacity: 1;
  transition: opacity 220ms ease;
}
.tier-accordion-body[hidden]{
  display: none;
}
.tier-accordion-line{
  font-size: var(--type-meta-size);
  font-weight: 400;
  color: var(--text-meta);
  line-height: 1.3;
}
.tier-accordion-line--strong{
  color: var(--text);
  font-weight: 500;
}
/* In-body switch CTA (non-current tiers only). Solid tier-accent pill;
   --tier-accent resolves from the .tier-{tier} class on the CTA itself. */
.tier-accordion-cta{
  margin-top: 4px;
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border: 0;
  border-radius: 10px;
  background: var(--tier-accent);
  color: #fff;
  font-size: var(--type-meta-size);
  font-weight: 500;
  cursor: pointer;
  transition: filter 140ms ease;
  /* Kill the native mobile tap-highlight — a translucent dark flash on
     tap that painted OVER the :active brighten and read as "darkens on
     click". With it gone, the brighten(1.14) press feedback shows. */
  -webkit-tap-highlight-color: transparent;
}
/* Solid tier-coloured pill: brighten on interaction (a "lift"), NOT
   darken. Darkening muddied the saturated tier colour and read as
   "pressed down"; brightening matches the app's hover-lightens
   philosophy for interactive surfaces. :active brightens a touch more
   as the press accent. (Deliberate divergence from the blue primary
   CTAs — Process / Continue — which darken; this is a distinct
   component family.) */
.tier-accordion-cta:hover,
.tier-accordion-cta:focus-visible{
  /* Re-assert the tier-accent fill: the global button:hover rule sets
     background var(--btn-hover) at specificity 0,1,1, which otherwise
     outranks the base .tier-accordion-cta background (0,1,0) and swaps
     the violet/pink/indigo for the dark hover grey — which the brighten
     was then lifting, so the button looked dark. This :hover selector
     (0,2,0) wins. */
  background: var(--tier-accent);
  filter: brightness(1.08);
  outline: none;
}
.tier-accordion-cta:active{
  background: var(--tier-accent);
  filter: brightness(1.14);
}
.tier-accordion-cta .ti{
  font-size: 15px;
}
@media (prefers-reduced-motion: reduce){
  .tier-accordion-chevron,
  .tier-accordion-chevron .ti,
  .tier-accordion-body,
  .tier-accordion-cta{
    transition: none;
  }
}

/* Trial-expired → Analyst tier row pulse (Fix 4, 2026-05-18).
   When a trial-expired Curator clicks the "Trial expired · Upgrade →" link
   on the Curator tier card, the click anchors to #tier-analyst on the
   same page. JS adds .tier-row-pulse for 1200ms to fire this single-
   expansion animation. NOT a loop — one beat, then settles.

   Pulse signal vocabulary: Analyst tier-accent (pink #F472B6) for both
   the box-shadow halo AND the background tint. Matches the row's
   identity (the row IS the Analyst row); reuses the existing
   --tier-accent var propagated by the .tier-analyst class on the
   button.

   Reduced-motion users: pulse keyframe suppressed via
   @media (prefers-reduced-motion: reduce). Scroll still happens
   (informational); the decorative pulse does not. */
@keyframes tierRowPulse {
  0% {
    box-shadow: 0 0 0 0 rgba(var(--tier-accent-rgb), 0);
    background: var(--card-rest);
  }
  20% {
    box-shadow: 0 0 0 6px rgba(var(--tier-accent-rgb), 0.18);
    background: rgba(var(--tier-accent-rgb), 0.08);
  }
  100% {
    box-shadow: 0 0 0 0 rgba(var(--tier-accent-rgb), 0);
    background: var(--card-rest);
  }
}
.account-plan-row.tier-row-pulse {
  animation: tierRowPulse 1200ms ease;
}
@media (prefers-reduced-motion: reduce) {
  .account-plan-row.tier-row-pulse {
    animation: none;
  }
}

/* Sign-out button (account page; the primary logout on mobile, where the bottom
   tab-bar has no logout). Neutral bordered button, red-tinted on hover. */
.account-signout-form{ margin: 4px 0 0; }
.account-signout-btn{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  padding: 11px 14px;
  font: inherit;
  font-size: var(--type-body-size);
  color: var(--text-meta);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  cursor: pointer;
  transition: background var(--motion-hover) ease, border-color var(--motion-hover) ease, color var(--motion-hover) ease;
}
.account-signout-btn svg{ width: 16px; height: 16px; }
.account-signout-btn:hover,
.account-signout-btn:focus-visible{
  color: var(--pill-failed-fg);
  border-color: var(--accent-red-soft);
  background: var(--btn-hover);
  outline: none;
}

/* Detail rows — neutral stripe, display-only (not editable). Hover-lightens
   the background so the rows feel inspectable rather than visually inert.
   No focus rule: the rows are <div>s, not focusable. No stripe colour
   change on hover: the row is not interactive in any state, so the stripe
   stays at --stripe-rest at all times. Editing profile fields (name,
   email) is a deferred product feature; precedent says it would land as
   a dedicated edit flow (cf. /auth/forgot for Change password), not as
   inline-editable inputs in this card. */
.account-detail-row{
  cursor: default;
}
.account-detail-row:hover{
  background: var(--card-hover);
}
.account-detail-label{
  font-size: var(--type-label-size);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-meta);
  min-width: 64px;
  flex-shrink: 0;
}

/* Card-level secondary action — sits inline with the section heading,
   right-aligned. Sentence case, meta-dim at rest, brightens on hover.
   Used by My Details → Change password; reusable for any future
   per-card secondary action that belongs in the header row. */
.account-card-action{
  margin-left: auto;
  font-size: var(--type-meta-size);
  font-weight: 400;
  color: var(--text-meta);
  text-decoration: none;
  line-height: 1.2;
  transition: color 140ms ease;
}
.account-card-action:hover,
.account-card-action:focus-visible{
  color: var(--text);
  text-decoration: none;
  outline: none;
}
/* Danger zone — self-service account deletion. The "Delete account" heading sits
   FLAT at the card top (like every other settings card); only the content below
   sits in a red accent-edge container (.edge-accent + the --danger variant), with
   a red outline button that fills on hover. */
.account-danger-body{ display: flex; flex-direction: column; gap: 10px; padding: 12px 16px; }
/* Armed (the typed email matches the account email): the muted grey edge +
   transparent bg flip to red edge + red tint. */
.account-danger-body.is-armed{ border-left-color: var(--pill-failed-fg); background: rgba(248, 113, 113, 0.10); }
.account-danger-copy{ margin: 0; font-size: var(--type-meta-size); color: var(--text-preview); line-height: 1.5; }
.account-danger-error{ margin: 0; font-size: var(--type-meta-size); color: var(--pill-failed-fg); font-weight: 600; }
.account-danger-label{ font-size: var(--type-meta-size); color: var(--text-meta); }
.account-danger-input{
  width: 100%; max-width: 320px; box-sizing: border-box; padding: 9px 12px;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px;
  color: var(--text); font-size: 0.9rem;
}
.account-danger-input:focus{ outline: none; border-color: var(--pill-failed-fg); }
.account-danger-input::placeholder{ color: var(--text-meta); }
/* Locked until the typed email matches the account email.
   Rest: grey muted border + grey muted text — reads inert/locked.
   Hover (still locked): border AND text go red (danger intent), but NEVER solid —
   a title/aria-label explains a valid account email is needed to unlock.
   Armed (valid email): solid red is the ONLY state that fills. */
.account-danger-btn{
  align-self: flex-start; padding: 9px 16px;
  border: 1px solid var(--border);
  border-radius: 8px; background: transparent; color: var(--text-meta);
  font-weight: 600; font-size: 0.875rem; cursor: pointer;
  transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
}
.account-danger-btn:hover,
.account-danger-btn:focus-visible{
  border-color: var(--pill-failed-fg); color: var(--pill-failed-fg);
  background: transparent; outline: none;
}
.account-danger-body.is-armed .account-danger-btn{
  border-color: var(--pill-failed-fg); background: var(--pill-failed-fg); color: #fff;
}
.account-detail-value{
  font-size: var(--type-body-size);
  color: var(--text);
  word-break: break-word;
  min-width: 0;
  flex: 1;
}

/* Pref rows (simplified 2026-06-08): Filter AND Display Preferences are now
   mutually-exclusive toggles with the same vocabulary — the SELECTED row's
   stripe is the user's TIER colour; non-selected rows stay muted. NO
   hover-preview (the earlier filter-pref "self selection" preview was too
   clever — the toggle switch shows state). The tier selectors (.account-plan-row)
   are the documented exception. --tier-accent cascades from body.tier-{tier}. */
.account-pref-row.is-active{
  border-left-color: var(--tier-accent);
}
/* Display Preferences row — binary toggle. Stripe reflects committed
   on/off state ONLY: hover does not preview the post-click colour, because
   the toggle switch itself is the user's source of truth for what the
   click will do. Deliberately diverges from the Filter Preferences hover
   rule above. The .is-active rule above still wins when the toggle is
   on, so the on-state stripe stays signature blue. */
.account-display-pref-row:hover:not(.is-active),
.account-display-pref-row:focus-visible:not(.is-active){
  border-left-color: var(--stripe-rest);
}
.account-pref-body{
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1;
}
.account-pref-title{
  font-size: var(--type-body-size);
  font-weight: 500;
  color: var(--text);
}
.account-pref-desc{
  font-size: var(--type-meta-size);
  color: var(--text-meta);
  line-height: 1.2;
  /* Reserve exactly two lines so every preference card is the same height at
     every width: a 1-line description still occupies two lines (min-height),
     and a description that would wrap to three is clamped to two (no overflow).
     Copy is kept short enough to never need the clamp's truncation. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  min-height: 2.4em; /* 2 lines × 1.2 line-height */
}

/* Display-size (UI zoom) stepper — a -/value/+ control sharing the
   .account-pref-row geometry. Not a toggle, so it carries no switch/is-active
   state; the stepper sits where the switch would. */
.account-ui-scale-row{ cursor: default; }
.ui-scale-stepper{
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
}
.ui-scale-btn{
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}
.ui-scale-btn:hover:not(:disabled){ background: var(--btn-hover); border-color: var(--border-2); }
.ui-scale-btn:disabled{ opacity: 0.4; cursor: default; }
.ui-scale-value{
  min-width: 46px;
  text-align: center;
  font-size: var(--type-body-size);
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.checkout-card-highlight{
  border-color: var(--accent-green-soft);
  background: var(--accent-green-bg);
}

/* Checkout card body — descriptive text matches Filter Preferences' .account-pref-desc
   vocabulary (preview-mid colour at body size). Wins over .muted's heavier hand. */
/* Billing card — manage-subscription portal button + status notices. */
.account-billing-notice{
  font-size: var(--type-meta-size);
  line-height: 1.3;
  color: var(--text-preview);
  margin: 0;
}
.account-billing-notice--ok{ color: var(--pill-done-fg); }
.account-billing-portal-form{ margin: 0; }
/* Billing content sits in a muted accent-edge container (mirrors .account-danger-body
   below); the "Billing" heading stays flat at the card top. */
.account-billing-body{ display: flex; flex-direction: column; justify-content: center; gap: 10px; padding: 12px 16px; min-height: 52px; box-sizing: border-box; }
.account-billing-copy{ margin: 0; font-size: var(--type-meta-size); color: var(--text-preview); line-height: 1.5; }
/* Portal button mirrors the Delete button shape, brand-blue instead of red:
   rest = hairline border + muted text + transparent; hover/focus = solid brand
   blue fill + white text. Standalone (NOT .auth-continue, whose base state is the
   disabled look). */
.account-billing-portal-btn{
  align-self: flex-start; display: inline-flex; align-items: center; gap: 6px;
  padding: 9px 16px;
  border: 1px solid var(--border);
  border-radius: 8px; background: transparent; color: var(--text-meta);
  font-weight: 600; font-size: 0.875rem; cursor: pointer;
  transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
}
.account-billing-portal-btn:hover,
.account-billing-portal-btn:focus-visible{
  border-color: var(--accent-blue); background: var(--accent-blue); color: #fff; outline: none;
}
.account-billing-portal-icon{ width: 0.9em; height: 0.9em; flex-shrink: 0; }
/* Prorated-amount line injected into the tier-change confirm dialog's extra slot. */
.confirm-proration-line{ margin: 10px 0 0; font-size: var(--type-meta-size); color: var(--text-preview); line-height: 1.5; }

/* Embedded Stripe Checkout overlay — mirrors .modal-overlay scroll-safety. */
.stripe-checkout-overlay{
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 24px 16px;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: rgba(0, 0, 0, 0.6);
}
.stripe-checkout-overlay[hidden]{ display: none; }
.stripe-checkout-panel{
  position: relative;
  width: 100%;
  max-width: 480px;
  margin: auto;
  background: var(--card-rest);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--shadow-lg);
  padding: 16px;
}
.stripe-checkout-close{
  position: absolute;
  top: 8px;
  right: 8px;
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: var(--text-meta);
  cursor: pointer;
  z-index: 1;
}
.stripe-checkout-close:hover{ background: var(--btn-hover); color: var(--text); }
.stripe-checkout-mount{ min-height: 320px; margin-top: 12px; }

.dashboard-card .muted{
  font-size: var(--type-body-size);
  line-height: 1.2;
}

.dashboard-section-head{
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  /* Lock to icon-btn height so sections without a "view all" icon still
     produce the same heading-row rhythm as Recent Saves / Trending. */
  min-height: var(--icon-btn-size);
}

.dashboard-head-link{
  color: var(--muted);
  text-decoration: none;
  font-size: var(--type-label-size);
  letter-spacing: var(--type-meta-track);
  text-transform: uppercase;
  line-height: 1.2;
}

.dashboard-head-link:hover{
  text-decoration: underline;
}

.admin-link-submit{
  border: 0;
  background: transparent;
  padding: 0;
  margin: 0;
  min-width: auto;
  min-height: 0;
  cursor: pointer;
}
/* Post-save confirmation: the "Saved" label reads success-green; reverts to the
   muted "Save …" label (default colour) as soon as a field is edited (JS toggles
   the class off in the dirty-check sync()). Three classes on the element (0,3,0)
   so it beats the `.admin-section-head .admin-section-action` rest colour (0,2,0)
   the button inherits inside the section-head row. */
.admin-section-action.admin-link-submit.is-saved-confirm{
  color: var(--pill-done-fg);
  font-weight: 500;
}

.dashboard-root{
  width: 100%;
}

.plan-limits-grid{
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
}

.plan-limit-tile{
  border-width: 1px;
}


.plan-limit-tile-curator{
  border-color: rgba(167, 139, 250, 0.78);
  background: linear-gradient(180deg, rgba(167, 139, 250, 0.26), rgba(167, 139, 250, 0.14));
}


.plan-limit-tile-analyst{
  border-color: rgba(244, 114, 182, 0.78);
  background: linear-gradient(180deg, rgba(244, 114, 182, 0.24), rgba(244, 114, 182, 0.14));
}

.plan-limit-tile-navigator{
  border-color: rgba(99, 102, 241, 0.82);
  background: linear-gradient(180deg, rgba(79, 70, 229, 0.26), rgba(79, 70, 229, 0.14));
}

.dashboard-root > *{
  width: 100%;
  max-width: none;
}

.dashboard-section{
  gap: 8px !important;
  margin-bottom: 12px;
  transition: border-color 200ms ease;
}
.dashboard-section:hover{
  border-color: rgba(var(--tier-accent-rgb), 0.25);
}

.dashboard-section-overview{
  gap: 10px !important;
}

.dashboard-section-last{
  margin-bottom: 0;
}

.dashboard-section-last .dashboard-metrics-grid{
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.dashboard-metrics-grid{
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
}
.dashboard-metrics-grid.dashboard-behaviour-grid{
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: stretch;
  grid-auto-rows: 1fr;
  row-gap: 6px;
  column-gap: 6px;
}
/* "Your status" grid (Dashboard) holds exactly two tiles (plan usage +
   connection) — lock to 2 columns at ALL widths so it fills the content column
   and matches the behaviour grid below (the base .dashboard-metrics-grid 3-col
   rule would otherwise leave a dead third column on the Dashboard). */
.dashboard-metrics-grid.dashboard-status-grid{
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* Match the behaviour grid's 6px gap (the base .dashboard-metrics-grid 8px
     left the two status tiles spaced wider than the compass grid below). */
  gap: 6px;
}

/* ------------------------------------------------------------------
   Tier tokens — shared by behaviour cards, tier strip, section hover
   ------------------------------------------------------------------ */
.tier-curator{
  --tier-accent: #A78BFA;
  --tier-accent-rgb: 167, 139, 250;
}
.tier-analyst{
  --tier-accent: #F472B6;
  --tier-accent-rgb: 244, 114, 182;
}
.tier-navigator{
  --tier-accent: #4F46E5;
  --tier-accent-rgb: 79, 70, 229;
}

/* ------------------------------------------------------------------
   Behaviour cards (Phase 1d)
   ------------------------------------------------------------------ */
.behaviour-card{
  border-radius: 1px 12px 12px 1px;
  transition: background 150ms ease;
}
.behaviour-card:hover{
  background: var(--surface);
}
.dashboard-section:focus-visible,
.behaviour-card:focus-visible,
.recent-save-row:focus-visible,
.recent-save-body:focus-visible{
  outline: 2px solid var(--tier-accent);
  outline-offset: 2px;
}

.behaviour-headline{
  font-size: 1.625rem;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
  letter-spacing: -0.01em;
}
.behaviour-headline-text{
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: 0;
}
.behaviour-sub{
  font-size: var(--type-meta-size);
}
.dashboard-card .behaviour-sub{
  font-size: var(--type-meta-size);
}

/* Streak dots */
.behaviour-streak-dots{
  display: flex;
  gap: 5px;
  margin-top: 4px;
  align-items: center;
}
.behaviour-streak-dot{
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--border);
  flex-shrink: 0;
  transition: background 150ms;
}
.behaviour-streak-dot.filled{
  background: var(--tier-accent);
}
.behaviour-streak-dot.today{
  width: 10px;
  height: 10px;
  background: var(--text);
  border: none;
  box-shadow: none;
}
.behaviour-streak-dot.today.filled{
  background: var(--text);
}

/* Weekday bar chart */
.behaviour-weekday-chart{
  display: flex;
  gap: 3px;
  height: 40px;
  margin-top: 4px;
  align-items: flex-end;
}
.behaviour-weekday-col{
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  flex: 1;
  height: 100%;
  justify-content: flex-end;
}
.behaviour-weekday-bar-wrap{
  flex: 1;
  display: flex;
  align-items: flex-end;
  width: 100%;
}
.behaviour-weekday-bar{
  width: 100%;
  min-height: 2px;
  border-radius: 2px 2px 0 0;
  background: var(--tier-accent);
  opacity: 0.4;
}
.behaviour-weekday-bar.peak{
  opacity: 1;
}
.behaviour-weekday-label{
  font-size: 0.55rem;
  color: var(--muted);
  text-align: center;
  flex-shrink: 0;
  line-height: 1;
}

/* Hours line chart. The SVG is pinned to the container box (position:absolute +
   inset:0) rather than sized by width/height:100% — a %-sized inline SVG with no
   intrinsic dimensions can fall back to its viewBox intrinsic size (100×100px) under
   a CSS `zoom`-ed ancestor (the in-app Display-size stepper) and overflow this 36px
   box. Pinning + overflow:hidden forces it to exactly the container at any zoom. */
.behaviour-line-chart{
  margin-top: 6px;
  height: 36px;
  width: 100%;
  position: relative;
  overflow: hidden;
}
.behaviour-line-svg{
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.behaviour-sub-secondary{
  font-size: 11px;
  color: var(--muted);
  opacity: 0.7;
  margin-top: 1px;
}

/* Progress bar (This Week) */
.behaviour-viz{
  margin-top: auto;
}
.behaviour-progress-track{
  position: relative;
  height: 4px;   /* match the trending / fav-creator / settings bars (4px) */
  background: rgba(255,255,255,0.08);
  border-radius: 2px;
  overflow: hidden;
}
/* Average (100%) notch — only on over-average weeks (.--over). Sits over the full
   fill at 100/pace_ratio %, a 2px page-bg gap so it reads as "the average line is
   here; the bar ran past it". */
.behaviour-progress-avg{
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--bg);
  transform: translateX(-1px);
}
.behaviour-progress-fill{
  height: 100%;
  background: var(--tier-accent);
  border-radius: 2px;
  transition: width 300ms ease;
}
@media (prefers-color-scheme: light){
  .behaviour-progress-track{
    background: rgba(0,0,0,0.06);
  }
}

/* ------------------------------------------------------------------
   Tier strip (Commit C)
   ------------------------------------------------------------------ */
.tier-strip{
  /* Three flex children: plus badge, tier identity (.tier-strip-main),
     CTA (.tier-strip-next / .tier-strip-state-cta). flex-wrap: wrap lets
     the CTA drop to its own row below ~640px via the media-query
     flex-basis override. Plus + identity always share row 1. */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  background: var(--surface);
  border: 0.5px solid var(--border);
  border-radius: 12px;
  margin-bottom: 14px;
  transition: border-color 150ms ease;
}
.tier-strip:hover{
  border-color: rgba(var(--tier-accent-rgb), 0.10);
}
.tier-strip-icon{
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: rgba(var(--tier-accent-rgb), 0.15);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--tier-accent);
  flex-shrink: 0;
}
.tier-strip-icon i{
  font-size: 14px;
}
.add-plus-circle{
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--tier-accent);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  text-decoration: none;
  /* Explicit transform-origin: center prevents sub-pixel jitter on
     :hover scale. CSS default is center; making it explicit guards
     against cascade overrides and documents intent. */
  transform-origin: center;
  transition: transform var(--motion-hover) ease, filter var(--motion-hover) ease;
}
.add-plus-circle svg{
  width: 14px;
  height: 14px;
  display: block;
}
.add-plus-circle:hover,
.add-plus-circle:focus-visible{
  transform: scale(1.05);
  filter: brightness(0.92);
}
@media (prefers-reduced-motion: reduce){
  .add-plus-circle{
    transition: filter var(--motion-hover) ease;
  }
  .add-plus-circle:hover,
  .add-plus-circle:focus-visible{
    transform: none;
  }
}
/* Disabled plus icon — at_limit OR trial_expired (any tier). Rendered as
   a <span>, NOT an anchor (the right-side CTA owns the upgrade
   affordance). Visual signal: neutral grey badge + meta-dim glyph,
   cursor: not-allowed. Overrides the base rule's var(--tier-accent)
   background and #fff glyph with the neutral pill-queued tokens
   (same hue as --text-meta, theme-aware low alpha — 0.15 dark, 0.20
   light). Disabled affordances are conventionally grey across UIs;
   tier-tinted-disabled was unconventional and risked reading as
   "still active but muted." Tier identity is preserved by the
   surrounding strip (identity text in tier-accent, CTA in amber for
   conversion-moment states). */
.add-plus-circle--disabled{
  background: var(--pill-queued-bg);
  color: var(--text-meta);
  cursor: not-allowed;
}
.add-plus-circle--disabled:hover,
.add-plus-circle--disabled:focus-visible{
  transform: none;
  filter: none;
}
.tier-strip-main{
  /* min-width: 0 lets the identity text shrink past its natural width
     when the CTA pill needs space, instead of pushing the CTA into
     overflow. Required for flex children with text content. */
  flex: 1 1 auto;
  min-width: 0;
}
.tier-strip-title{
  font-size: 13px;
  font-weight: 500;
  line-height: 1.2;
}
.tier-strip-title span{
  color: var(--tier-accent);
}
.tier-strip-sub{
  font-size: 11px;
  /* Verb tagline reads in the body-copy colour (--muted / --text-meta),
     matching the library-row excerpt and the account-plan tagline elsewhere —
     the tier name span (.tier-strip-title span) carries the only accent
     colour, the verb <strong> the only weight. (Earlier this line was bright
     --text; aligned to body copy 2026-06-06 so the supporting tagline doesn't
     compete with the identity line.) */
  color: var(--muted);
  line-height: 1.3;
  margin-top: 1px;
}
.tier-strip-sub strong{
  font-weight: 500;
  color: var(--muted);
}
.tier-strip-next{
  font-size: 11px;
  color: var(--muted);
  padding: 3px 8px;
  border: 0.5px solid var(--border);
  border-radius: 4px;
  flex-shrink: 0;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  transition: border-color 200ms ease, color 200ms ease;
}
.tier-strip-next:hover{
  border-color: var(--tier-next-accent);
  color: var(--tier-next-accent);
}
.tier-strip-next-arrow{
  width: 12px;
  height: 12px;
  display: block;
}

/* ------------------------------------------------------------------
   Tier strip state-driven amber treatment.
   State 1 (within_limit): no modifier class. Default chrome.
   State 2 (approaching_limit): subtle amber CTA only.
   State 3 (at_limit): modifier class on container as semantic hook only.
                       Container uses standard chrome (border, radius,
                       bg) — NO amber stripe, NO amber tint. Amber lives
                       exclusively on the CTA element (see
                       .tier-strip-state-cta--at-limit below). On the
                       account page (/ui/upgrade) the tier strip is
                       hidden in this state — MY PLAN provides richer
                       state information there.
   State 4 (trial_expired): same as State 3 — semantic modifier class
                            on container only; amber on CTA only.
                            Account page tier strip also hidden.
                            CTA points to /ui/upgrade#tier-analyst.
   ------------------------------------------------------------------ */

.tier-strip-state-cta{
  font-size: 11px;
  color: var(--muted);
  padding: 3px 8px;
  border: 0.5px solid var(--border);
  border-radius: 4px;
  flex-shrink: 0;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  transition: border-color 140ms ease, color 140ms ease, background 140ms ease;
}
.tier-strip-state-cta-arrow{
  width: 12px;
  height: 12px;
  display: block;
}
.tier-strip-state-cta-arrow.ti{
  font-size: 12px;
  line-height: 1;
}
.tier-strip-state-cta--approaching-limit{
  color: var(--pill-processing-fg);
  background: rgba(251, 191, 36, 0.03);
  border-color: rgba(251, 191, 36, 0.20);
}
.tier-strip-state-cta--approaching-limit:hover{
  background: rgba(251, 191, 36, 0.08);
  border-color: rgba(251, 191, 36, 0.35);
}
.tier-strip-state-cta--at-limit,
.tier-strip-state-cta--trial-expired{
  color: var(--pill-processing-fg);
  background: rgba(251, 191, 36, 0.12);
  border-color: rgba(251, 191, 36, 0.30);
}
.tier-strip-state-cta--at-limit:hover,
.tier-strip-state-cta--trial-expired:hover{
  background: rgba(251, 191, 36, 0.20);
  border-color: rgba(251, 191, 36, 0.45);
}

/* Neutral CTA — Analyst at_limit ("Upgrade to Navigator") and Navigator
   at_limit ("Contact support" mailto). Paid users on a soft ceiling don't
   warrant the conversion-moment amber chrome reserved for Curator's trial.
   Outlined pill, meta-dim text at rest, brightens on hover. Matches the
   neutral-secondary-action vocabulary used by `.account-card-action` and
   `.admin-section-action`. */
.tier-strip-state-cta--neutral{
  color: var(--text-meta);
  background: transparent;
  border-color: var(--border);
}
.tier-strip-state-cta--neutral:hover{
  color: var(--text);
  background: var(--btn-hover);
  border-color: var(--border-2);
}

/* Narrow-viewport stack: tier identity stays on row 1 (alongside the
   plus badge); CTA wraps to a full-width row 2. Achieved by giving the
   CTA flex-basis: 100% at ≤640px so .tier-strip's existing flex-wrap
   pushes it to its own row. Pill width stays natural (left-aligned in
   that row via default flex-start). Y-axis spacing between rows comes
   from the parent's row-gap (inherits 10px from gap shorthand). This
   is an X-axis-only mobile concession — no Y-axis overrides. */
@media (max-width: 640px){
  .tier-strip-next,
  .tier-strip-state-cta{
    flex: 1 0 100%;
  }
}

/* PR C Commit 4 — tier strip is hidden entirely at the canonical mobile
   breakpoint (≤560px). Mobile-web minimises persistent chrome: the strip's
   at-limit role is carried by the Library at-limit banner (Commit 1), the
   last-save moment by the detail-page toast (Commit 3), and the add-save
   plus was the only in-app mobile add affordance — intentionally removed
   (mobile add is share-extension-only). Applies to all six include sites
   uniformly. The 640px CTA-wrap rule above still covers the 561–640px band
   where the strip remains visible. */
@media (max-width: 560px){
  .tier-strip{
    display: none;
  }
}

/* ── Unified top strip (2026-06-09) ─────────────────────────────────────────
   Brand wordmark + search entry + an add-save PLUS, rendered ONCE from the
   chrome_context global at the top of .main on every authenticated page (both
   breakpoints). Non-sticky: scrolls with content. Supersedes the per-surface
   .tier-strip include AND the mobile .mobile-brand header.
   Layout: [brand] · [search] · [plus]. The strip carries NO identity/CTA text —
   tier identity lives on the Dashboard "Your status" card. The plus is the only
   state the strip shows (brand-blue available / muted at cap). The at-limit /
   approaching / trial-expired WARNING is the separate .top-strip-banner row
   below (between the header and the first content block). */
.top-strip{
  display: flex;
  /* nowrap so the search ALWAYS sits on the same row as the brand (it never drops
     to its own row); it grows to fill the remaining width, floored at the
     helper-text length via min-width: max-content. */
  flex-wrap: nowrap;
  align-items: center;
  gap: 16px;
  margin-bottom: 16px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--border);
}
.top-strip-brand{
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  text-decoration: none;
  color: var(--text);
}
.top-strip-brand .brand-logo{ width: 128px; height: auto; display: block; }
.top-strip-search{
  display: inline-flex;
  align-items: center;
  gap: 8px;
  /* Grows to fill whatever room is available (the gap between brand and plus on
     desktop; the row after the logo on mobile). A small min-width floor keeps it
     usable if room is very tight. */
  flex: 1 1 auto;
  min-width: 6rem;
  /* Height locked to the plus icon's 28px diameter (the two sit side by side in
     the strip). Vertical padding 0 + box-sizing:border-box so the 0.5px border
     and the centred input/icon all fit inside 28px. */
  height: 28px;
  padding: 0 14px;
  background: var(--surface-2);
  /* 0.5px hairline to match the rounded cards (.card); hover/focus change only
     the COLOUR, never the width, so the hairline persists. */
  border: 0.5px solid var(--border);
  /* Full lozenge (circular-ended pill). The caret clipping was fixed separately
     by `overflow: visible` + the input's left padding (below), NOT by reducing
     this radius. */
  border-radius: 999px;
  color: var(--text-meta);
  text-decoration: none;
  font-size: 0.85rem;
  transition: border-color 150ms ease, color 150ms ease;
}
.top-strip-search:hover{ color: var(--text); border-color: var(--border-2); }
.top-strip-search-icon{ width: 16px; height: 16px; flex-shrink: 0; }
/* Plus = the only state the strip shows. Brand-blue (the wordmark triangle blue,
   NOT tier-accent) when the user can add a save; the shared .add-plus-circle base
   is overridden here. At cap / trial-expired it falls to .add-plus-circle--disabled
   (muted grey, inert) via the partial. */
.top-strip-plus{ flex-shrink: 0; margin-left: auto; }
.top-strip-plus.add-plus-circle{ background: #0698ea; }
/* Cancel the shared .add-plus-circle:hover scale(1.05) for the strip plus — on a
   small glyph it sub-pixel-shifts the + at non-100% browser zoom (a paint vs
   layout-jitter call: brightness alone is enough feedback here). */
.top-strip-plus.add-plus-circle:hover,
.top-strip-plus.add-plus-circle:focus-visible{ filter: brightness(0.92); transform: none; }
.top-strip-plus.add-plus-circle--disabled{ background: var(--pill-queued-bg); color: var(--text-meta); }

/* Warning row — a full-width amber banner BETWEEN the strip and the first content
   block (the strip never carries the warning itself). Wraps to two lines on a
   narrow viewport (the copy is white-space: normal) so it never overflows. Reuses
   the amber edge-accent vocabulary. --subtle = approaching-limit (softer fill). */
.top-strip-banner{
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  margin-bottom: 16px;
  padding: 10px 14px 10px 11px;
  border-left: 3px solid var(--pill-processing-fg);
  border-radius: 0 10px 10px 0;
  background: rgba(251, 191, 36, 0.12);
  color: var(--text);
  text-decoration: none;
  font-size: var(--type-body-size);
  line-height: 1.4;
  transition: background 140ms ease;
}
a.top-strip-banner:hover{ background: rgba(251, 191, 36, 0.18); }
.top-strip-banner--subtle{ background: rgba(251, 191, 36, 0.05); }
a.top-strip-banner--subtle:hover{ background: rgba(251, 191, 36, 0.10); }
.top-strip-banner--inert{ cursor: default; }
/* Founding-Beta identity banner — Navigator-level accent (a perk, not a
   warning), overriding the amber default. The member's <body> class is
   tier-curator (their real billing tier), so the navigator indigo is keyed
   explicitly here rather than via --tier-accent. */
.top-strip-banner--founding{
  border-left-color: #4F46E5;
  background: rgba(79, 70, 229, 0.12);
}
a.top-strip-banner--founding:hover{ background: rgba(79, 70, 229, 0.18); }
.top-strip-banner--founding .top-strip-banner-arrow{ color: #4F46E5; }
html[data-theme="light"] .top-strip-banner--founding{
  border-left-color: #4338CA;
  background: rgba(67, 56, 202, 0.10);
}
html[data-theme="light"] a.top-strip-banner--founding:hover{ background: rgba(67, 56, 202, 0.16); }
html[data-theme="light"] .top-strip-banner--founding .top-strip-banner-arrow{ color: #4338CA; }
.top-strip-banner-copy{
  flex: 1;
  min-width: 0;
  font-weight: 500;
  white-space: normal;   /* wrap to two lines on narrow viewports */
}
.top-strip-banner-arrow{
  flex-shrink: 0;
  font-size: 16px;
  color: var(--pill-processing-fg);
}

/* Mobile (≤560px): brand + search only — the PLUS is HIDDEN (mobile add is
   share-extension-only, canon). The search keeps flex-grow:1, so it extends to
   fill the row after the logo; the @container rule above degrades the placeholder
   to "Search…" if that filled width is still too narrow for the full label. */
@media (max-width: 560px){
  .top-strip{ gap: 12px 10px; margin-bottom: 14px; }
  .top-strip-brand .brand-logo{ width: 110px; }
  .top-strip-plus{ display: none; }
}

/* ------------------------------------------------------------------
   Account-page MY PLAN usage block — verbose progress + status surface.
   Folded into the existing My Plan card on /ui/upgrade above the tier
   row list; the tier strip carries the at-a-glance state elsewhere.
   ------------------------------------------------------------------ */
.account-plan-usage{
  padding: 0;
}
.account-plan-usage-meta{
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  flex-wrap: wrap;
}
.account-plan-usage-count{
  font-size: var(--type-body-size);
  font-weight: 500;
  color: var(--text);
}
.account-plan-usage-status{
  font-size: var(--type-meta-size);
  color: var(--text-meta);
}
.account-plan-usage-status-amber{
  color: var(--pill-processing-fg);
}
.account-plan-usage-track{
  position: relative;
  width: 100%;
  height: 4px;
  background: var(--surface-2);
  border-radius: 999px;
  overflow: hidden;
}
.account-plan-usage-bar{
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  /* Per-tier accent fill (Curator purple / Analyst pink / Navigator indigo),
     resolved from the body per-tier class via --tier-accent. */
  background: var(--tier-accent);
  border-radius: 999px;
  transition: width 200ms ease;
}
/* Nominal presence fill — Navigator channels bar at 1+ channels. Unlimited
   cap means there's no quantitative position to plot, so a fixed small fill
   reads as "you have channels" (presence), not "approaching cap" (progress).
   Fixed width at any count (5, 50, 500 → same fill); tier-accent via the base
   rule. Progress bars stay tier-coloured in ALL states — the amber action
   signal lives on the banner above the card, not duplicated on the bars. */
.account-plan-usage-bar--nominal{
  width: 12%;
}
/* Over-cap red overlay removed (R1 completion): progress bars stay
   tier-coloured in ALL cap states, including over-cap (e.g. a dev/admin
   account at 40/10). The red --pill-failed-fg overlay at 0.5 opacity layered
   over the purple fill produced a pink cast that read as the wrong tier
   accent. Over-cap is still communicated by the numerator ("40 / 10 saves")
   above the 100%-filled bar + the unified banner above the card — no
   redundant colour-shift on the bar itself. */
.account-plan-usage + .account-row-list{
  margin-top: 8px;
}
/* MY PLAN dual progress bars — saves + channels. Adjacent on desktop, each
   unit flexing to share the row width; STACKED on mobile. The flex-direction
   row→column reshape + the gap change are an intentional layout-axis reshape
   (the two bars go side-by-side → stacked), the same documented exception the
   .tier-strip-next mobile reshape uses — NOT a vertical-rhythm tweak the
   X-axis-only mobile rule forbids. Each .account-plan-usage unit is a self-
   contained label-above-bar block, so stacking preserves readability with no
   markup change. */
.my-plan-progress-row{
  display: flex;
  flex-direction: row;
  gap: 16px;
  /* Breathing room below the progress section before the tier comparison
     cards (R5). 16px token + the card's 8px stack gap → ~24px effective
     separation. On mobile (stacked bars) this sits below the lower (channels)
     bar. Token-aligned: the scale tops at --space-6 (16px); no 24-40px token
     exists, so the effective gap is composed, not a single off-scale value. */
  margin-bottom: var(--space-6);
}
.my-plan-progress-row .account-plan-usage{
  flex: 1 1 0;
  min-width: 0;
}
@media (max-width: 560px){
  .my-plan-progress-row{
    flex-direction: column;
    gap: 8px;
  }
}
.account-plan-interest-map-link{
  font-size: 13px;
  font-weight: 400;
  color: var(--text-meta);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-top: 12px;
  margin-bottom: 16px;
  transition: color 140ms ease;
}
.account-plan-interest-map-link:hover{
  color: var(--text-body);
}

.dashboard-metric-tile{
  border: 0.5px solid var(--border);
  border-radius: 10px;
  padding: 8px 10px;
  background: var(--surface-2);
  display: flex;
  flex-direction: column;
  gap: 4px;
  position: relative;
}
.dashboard-metric-tile.behaviour-card{
  border-radius: 0 12px 12px 0;
  border-top: none;
  border-right: none;
  border-bottom: none;
  border-left: 3px solid var(--tier-accent);
}
.dashboard-metric-tile.behaviour-card::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 12px 12px 0;
  pointer-events: none;
}

.status-card{
  border-radius: 0 12px 12px 0;
  transition: background 150ms ease, border-left-color 150ms ease;
}
.dashboard-metric-tile.status-card{
  border-top: none;
  border-right: none;
  border-bottom: none;
  /* Persistent tier-accent edge (2026-06-09) — matches the sibling Dashboard cards
     (behaviour grid, Things-you-like) on this reflective surface. Previously muted
     at rest (--stripe-rest) → tier on hover, which fit the Home "selected = tier"
     vocabulary; on the Dashboard the edge accent reads as a card identity and
     should persist like its neighbours. */
  border-left: 3px solid var(--tier-accent);
}
.dashboard-metric-tile.status-card::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 12px 12px 0;
  pointer-events: none;
}
.status-card:hover{
  background: var(--surface);
}
.dashboard-metric-usage:hover{
  border-left-color: var(--tier-accent);
}

/* PR B Commit 2 — My Plan + Connection tiles share a leading-glyph eyebrow.
   The glyph inherits --text-meta from the .muted eyebrow (matches the
   Streak/Hours behaviour-card eyebrow treatment); flex keeps the SVG IG mark
   and the Tabler tier glyph baseline-aligned with the eyebrow text. */
.dashboard-status-eyebrow{
  display: flex;
  align-items: center;
  gap: 6px;
}
/* The IG eyebrow glyph is a 14px SVG; the sibling eyebrow icons on THIS surface
   (plan tile + behaviour grid) are Tabler font glyphs at the small's font-size
   (~12px), so the SVG reads slightly bigger. Trim it to 12px to match — scoped to
   the dashboard status eyebrow ONLY, so the same platform glyph on library rows /
   creator cards (14px elsewhere) is untouched. */
.dashboard-status-eyebrow .library-eyebrow-platform svg{ width: 12px; height: 12px; }
/* Connection tile — awareness-only; whole tile links to the account card.
   Static eyebrow ("Session cookies") + state-coloured primary content row +
   tinted background per state. Tint rgba values reuse the /ui/items/new
   connection-row treatment verbatim (.new-item-status-row-ok / -off,
   styles.css ~5860-5866); the amber expired variant has no /ui/items/new
   analog and uses the design-language --pill-processing-fg hue at 6%. */
.dashboard-connection-tile{
  text-decoration: none;
  color: inherit;
}
.dashboard-connection-status{
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.dashboard-connection-cta{
  font-size: var(--type-meta-size);
  color: var(--text-meta);
}
/* Rest state is NEUTRAL — no tint background, white status text + tick. The state
   colour (the bg tint, the status text/tick, and the edge accent — green / amber /
   red) appears ONLY on hover, so the resting card reads calm and uniform with its
   sibling tile and the colour is an interaction reveal. This avoids the at-rest
   colour-disparity distraction. Applies to all three states (incl. failure). */
.dashboard-connection-status{ color: var(--text); }
.dashboard-connection-tile--connected:hover{
  background: rgba(74, 222, 128, 0.10);
  border-left: 3px solid var(--pill-done-fg);
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}
.dashboard-connection-tile--connected:hover .dashboard-connection-status{ color: var(--pill-done-fg); }
.dashboard-connection-tile--expired:hover{
  background: rgba(251, 191, 36, 0.10);
  border-left: 3px solid var(--pill-processing-fg);
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}
.dashboard-connection-tile--expired:hover .dashboard-connection-status{ color: var(--pill-processing-fg); }
.dashboard-connection-tile--disconnected:hover{
  background: rgba(248, 113, 113, 0.10);
  border-left: 3px solid var(--pill-failed-fg);
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}
.dashboard-connection-tile--disconnected:hover .dashboard-connection-status{ color: var(--pill-failed-fg); }

/* Per-user-first onboarding (2026-06-10): action-needed states shout AT REST (not
   hover-only) so a new user (disconnected) or a lapsed user (expired) can't miss
   that they need to connect their own Instagram session — the healthy connected
   state stays calm/hover-reveal. Tint + coloured status + a BRIGHT actionable CTA
   ("Connect →"/"Reconnect →"); the edge stripe stays a hover accent to avoid an
   at-rest layout shift vs the sibling plan-usage tile. */
.dashboard-connection-tile--disconnected{ background: rgba(248, 113, 113, 0.10); }
/* .status-card carries a persistent border-left:3px var(--tier-accent); override
   just the COLOUR (no layout shift) so the left edge reads red/amber AT REST for
   the action-needed states, matching the sibling tile's edge-accent vocabulary.
   Two classes so it beats .dashboard-metric-tile.status-card. */
.dashboard-connection-tile--disconnected.status-card{ border-left-color: var(--pill-failed-fg); }
.dashboard-connection-tile--disconnected .dashboard-connection-status{ color: var(--pill-failed-fg); }
/* CTA stays muted (= the base --text-meta, identical to the sibling "N remaining"
   .behaviour-sub) — the red status text + edge + tint carry the action-needed
   loudness; the CTA reads as a quiet sub, aligning with the muted subs on the
   revised IG connect buttons. */
.dashboard-connection-tile--expired{ background: rgba(251, 191, 36, 0.10); }
.dashboard-connection-tile--expired.status-card{ border-left-color: var(--pill-processing-fg); }
.dashboard-connection-tile--expired .dashboard-connection-status{ color: var(--pill-processing-fg); }

/* Guided IG connect wizard (global modal). Privacy-forward lead + a numbered
   3-step list reusing the tier-accent number badge vocabulary from the onboarding
   guides. */
/* .modal-card is NOT a flex container, so the card must declare its own column
   flow for the gap to apply (otherwise the children collapse to default block
   margins — the "far too tight" spacing). */
.ig-connect-card{ display: flex; flex-direction: column; gap: 18px; max-width: 460px; padding: 22px; }
.ig-connect-title{ display: block; }
.ig-connect-why{ margin: 0; color: var(--text-preview); font-size: var(--type-body-size); line-height: 1.5; }
.ig-connect-why em{ font-style: italic; color: var(--text); }
/* The hidden upload form carries no visual box, so keep it OUT of the flex flow
   (display:contents) — otherwise it adds an empty 18px gap on each side. */
.ig-connect-card > form{ display: contents; }
.ig-connect-steps{ list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 18px; }
.ig-connect-step{ display: flex; align-items: flex-start; gap: 12px; }
.ig-connect-num{
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; min-width: 22px; flex: 0 0 22px; aspect-ratio: 1;
  border-radius: 50%; line-height: 1;
  background: var(--tier-accent, #0698ea); color: #fff;
  font-size: 0.72rem; font-weight: 600; margin-top: 1px;
}
.ig-connect-step-text{ display: flex; flex-direction: column; gap: 4px; font-size: var(--type-body-size); line-height: 1.4; }
/* Glyph aligns to the FIRST line of the step title (flex-start), so when the
   title wraps to two lines at a narrow width the glyph stays on line 1 — never
   floats to the vertical centre between the lines. The 18px glyph is ~the title
   line-box height, so a 1px nudge optically centres it on that first line. */
/* DIRECT child only — the title strong (glyph + label). Inline <strong> emphasis
   inside the body copy (.muted) must stay inline, else words like "instagram.com"
   / "Netscape" become flex blocks and force their own line. */
.ig-connect-step-text > strong{ display: flex; align-items: flex-start; gap: 7px; }
.ig-connect-glyph{ height: 18px; width: auto; flex-shrink: 0; margin-top: 1px; color: var(--tier-accent, #0698ea); }
/* instagram.com is a link: bold like the surrounding emphasis at rest, tier on hover. */
.ig-connect-domain{ color: inherit; font-weight: 600; text-decoration: none; transition: color 140ms ease; }
.ig-connect-domain:hover{ color: var(--tier-accent, #0698ea); }
/* Onboarding brightness: the step body copy reads at --text-preview (not the
   modal default --text-meta) — these are the most useful words a new user sees. */
.ig-connect-step-text .muted{ font-size: var(--type-meta-size); line-height: 1.5; color: var(--text-preview); }
.ig-connect-step-text code{ font-size: 0.85em; padding: 1px 4px; border-radius: 4px; background: var(--surface-2); }
.ig-connect-link{ color: var(--tier-accent, #0698ea); text-decoration: none; white-space: nowrap; transition: color 140ms ease; }
.ig-connect-link:hover{ color: var(--text); }

/* "Initial setup" connect card on Home (Phase 3). Uses the EDGE-ACCENT
   connection vocabulary (muted stripe at rest → red on hover) to match the other
   connection elements (the Dashboard health tile), NOT a generic action button —
   a clickable connection STATUS is the documented edge-accent exception (like the
   at-limit banners). Paint-only hover (canon). Falls away once connected. */
/* The shared IG "Connect Instagram" button (_ig_connect_card.html) — homepage
   Initial-setup, new-item page, and Settings Connection card all render this.
   Fixed 52px so every IG connect button matches (the dashboard health TILE is the
   deliberate exception). icon + title + sub + arrow; the sub is clamped to one
   line so the 52px contract holds at any width. */
.ig-setup-card{
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  text-align: left;
  min-height: 52px;
  padding: 8px 14px;
  box-sizing: border-box;
  cursor: pointer;
}
.ig-setup-card:hover{
  border-left-color: var(--pill-failed-fg);
  background: rgba(248, 113, 113, 0.10);
}
.ig-setup-card:hover .ig-setup-arrow{ color: var(--pill-failed-fg); }
/* Alert variant — red AT REST (not just hover). Used on the functional surfaces
   (new-item + Settings) where disconnected is an active blocker; the homepage
   Initial-setup stays muted (warm onboarding). Two-class specificity (0,2,x) so
   it beats .edge-accent--muted's grey edge / transparent bg (0,1,0) and the
   muted hover. Edge + soft tint carry the signal; the title/sub stay normal
   colour so it reads "needs attention", not "error". */
.ig-setup-card.ig-setup-card--alert{
  border-left-color: var(--pill-failed-fg);
  background: rgba(248, 113, 113, 0.10);
}
.ig-setup-card.ig-setup-card--alert:hover{
  background: rgba(248, 113, 113, 0.16);
}
.ig-setup-card.ig-setup-card--alert .ig-setup-arrow{ color: var(--pill-failed-fg); }
/* Connected variant (ig_connected_card) — the same card shape, GREEN: green edge +
   soft green tint + green glyph/title. It's a STATUS, not a click target (only the
   disconnect button inside is interactive), so cursor:default and no red hover.
   Two-class specificity (0,2,x) beats the base edge + the base :hover. */
.ig-setup-card.ig-setup-card--ok{
  border-left-color: var(--pill-done-fg);
  background: rgba(74, 222, 128, 0.06);
  cursor: default;
}
.ig-setup-card.ig-setup-card--ok:hover{
  border-left-color: var(--pill-done-fg);
  background: rgba(74, 222, 128, 0.06);
}
.ig-setup-card--ok .ig-setup-icon{ color: var(--pill-done-fg); }
.ig-setup-card--ok .ig-setup-title{ color: var(--pill-done-fg); }
/* Disconnect action sits in the arrow's slot — pushed right by the body's flex:1. */
.ig-setup-disconnect-form{ flex-shrink: 0; margin: 0; }
/* No backplate tile — the IG glyph sits bare. */
.ig-setup-icon{ display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; color: var(--text); }
/* 18px to match the nav glyphs (was 26px). */
.ig-setup-icon svg{ width: 18px; height: 18px; }
.ig-setup-body{ display: flex; flex-direction: column; gap: 2px; flex: 1 1 auto; min-width: 0; }
/* Reads at --text-preview (not white) so its VISUAL weight matches the other
   onboarding step labels — it's a step in the process, not a headline. */
.ig-setup-title{ font-weight: 500; color: var(--text-preview); }
/* Muted (not preview) — button-instruction sub-lines read muted, like the
   .how-to-save-sub step subs; only the wider instructional body copy is preview. */
/* One line, ellipsis-clipped — keeps the 52px height contract on every width. */
.ig-setup-sub{ font-size: var(--type-meta-size); color: var(--text-meta); line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.ig-setup-arrow{ flex-shrink: 0; color: var(--tier-accent, #0698ea); font-size: 1.05rem; transition: color 140ms ease; }

/* Attention pulse (homepage Initial-setup connect card, disconnected). A smooth
   two-cycle "breath" of the red attention tint + edge on load, then it settles to
   the muted rest — drawing the eye to the one priority action without the
   gating/dimming approach. Added by the dashboard script ONCE per session
   (sessionStorage). Paint-only (background/edge/box-shadow) so no layout shift;
   the arrow swells in step. iteration-count 2 = exactly two pulses. */
@keyframes ig-setup-pulse{
  0%, 100% { background: transparent; border-left-color: var(--stripe-rest); box-shadow: 0 0 0 0 rgba(248, 113, 113, 0); }
  50%      { background: rgba(248, 113, 113, 0.14); border-left-color: var(--pill-failed-fg); box-shadow: 0 0 0 3px rgba(248, 113, 113, 0.12); }
}
@keyframes ig-setup-pulse-arrow{
  0%, 100% { color: var(--tier-accent, #0698ea); }
  50%      { color: var(--pill-failed-fg); }
}
.ig-setup-card--pulse{ animation: ig-setup-pulse 0.85s ease-in-out 2; }
.ig-setup-card--pulse .ig-setup-arrow{ animation: ig-setup-pulse-arrow 0.85s ease-in-out 2; }
@media (prefers-reduced-motion: reduce){
  .ig-setup-card--pulse,
  .ig-setup-card--pulse .ig-setup-arrow{ animation: none; }
}

.dashboard-section-overview .dashboard-metric-tile{
  border-color: rgba(6, 152, 234, 0.42);
  background: rgba(6, 152, 234, 0.1);
}

.dashboard-section-overview .dashboard-metric-tile .dashboard-metric-delta,
.dashboard-section-overview .dashboard-metric-tile .dashboard-mini-track{
  margin-top: 2px;
}

.dashboard-section-overview .dashboard-metric-usage .dashboard-mini-track{
  transform: translateY(4px);
}




.dashboard-section-overview .dashboard-metric-tile.plan-limit-tile-curator{
  border-color: rgba(167, 139, 250, 0.78);
  background: linear-gradient(180deg, rgba(167, 139, 250, 0.26), rgba(167, 139, 250, 0.14));
}


.dashboard-section-overview .dashboard-metric-tile.plan-limit-tile-analyst{
  border-color: rgba(244, 114, 182, 0.78);
  background: linear-gradient(180deg, rgba(244, 114, 182, 0.24), rgba(244, 114, 182, 0.14));
}

.dashboard-section-overview .dashboard-metric-tile.plan-limit-tile-navigator{
  border-color: rgba(99, 102, 241, 0.82);
  background: linear-gradient(180deg, rgba(79, 70, 229, 0.26), rgba(79, 70, 229, 0.14));
}

.dashboard-metric-tile small{
  font-size: var(--type-body-size);
  line-height: 1.2;
}

.dashboard-metric-tile strong{
  font-size: var(--type-body-size);
  font-weight: 700;
  line-height: 1.2;
}

.dashboard-metric-delta{
  font-size: var(--type-meta-size);
  letter-spacing: 0.01em;
  line-height: 1.2;
  color: var(--muted);
}

.dashboard-metric-delta.up{
  color: var(--accent-blue);
}

.dashboard-metric-delta.down{
  color: #f59e0b;
}

.dashboard-metric-delta.flat{
  color: var(--muted);
}

.dashboard-mini-track{
  width: 100%;
  height: 4px;   /* match the trending / fav-creator / settings bars (4px) */
  border-radius: 999px;
  background: rgba(255,255,255,0.12);
  overflow: hidden;
  margin-top: 2px;
  display: flex;
  align-items: stretch;
}

html[data-theme="light"] .dashboard-mini-track{
  background: rgba(17, 44, 74, 0.24);
}

.dashboard-mini-bar{
  display: block;
  height: 100%;
  background: #0698ea;
}

.dashboard-mini-bar-over{
  display: block;
  height: 100%;
  background: #ef4444;
  cursor: help;
}


.dashboard-metric-usage-curator .dashboard-mini-bar{
  background: #a78bfa;
}


.dashboard-metric-usage-analyst .dashboard-mini-bar{
  background: #f472b6;
}

.dashboard-metric-usage-navigator .dashboard-mini-bar{
  background: #6366f1;
}


.plan-card-usage-curator .dashboard-mini-bar{
  background: #a78bfa;
}


.plan-card-usage-analyst .dashboard-mini-bar{
  background: #f472b6;
}

.plan-card-usage-navigator .dashboard-mini-bar{
  background: #6366f1;
}

.dashboard-metric-health.good:hover{
  border-left-color: var(--pill-done-fg);
}
.dashboard-metric-health.warn:hover{
  border-left-color: var(--pill-processing-fg);
}
.dashboard-metric-health.attention:hover,
.dashboard-metric-health.danger:hover{
  border-left-color: var(--pill-failed-fg);
}

.dashboard-health-cta{
  margin-top: 2px;
}

.dashboard-metric-health.danger .dashboard-health-cta{
  color: #fecaca;
  border-color: rgba(239, 68, 68, 0.55);
  background: rgba(239, 68, 68, 0.14);
}

.dashboard-metric-health.warn .dashboard-health-cta{
  color: #fde68a;
  border-color: rgba(245, 158, 11, 0.55);
  background: rgba(245, 158, 11, 0.14);
}

html[data-theme="light"] .dashboard-metric-health.danger .dashboard-health-cta{
  color: #991b1b;
}

html[data-theme="light"] .dashboard-metric-health.warn .dashboard-health-cta{
  color: #78350f;
}

.dashboard-pill-cloud{
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 6px;
  row-gap: 6px;
  column-gap: 6px;
  margin-top: 0;
}

.dashboard-actions-list{
  gap: 8px;
}

.dashboard-action-row{
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border: 0.5px solid var(--border);
  border-radius: 10px;
  background: var(--surface-2);
}

.recent-saves-list{
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 6px;
}

.recent-save-row{
  display: flex;
  /* flex-start (was center) so the title + body meet the TOP of the thumb,
     matching the compact Library row. The action row is absolutely
     positioned at the body's bottom (min-height: 59px = thumb height), so
     padding stays symmetric 8/8 — the out-of-flow action row carries no
     bottom airspace to compensate for (see .library-list--compact). */
  align-items: flex-start;
  gap: 10px;
  padding: 8px 12px 8px 12px;
  background: var(--surface-2);
  /* Accent edge neutral at rest → tier colour on hover/focus, matching the
     Library row + the app-wide "selected = tier colour" vocabulary (the tier
     colour is the interaction/selected signal, not the resting default). */
  border-left: 3px solid var(--stripe-rest);
  border-radius: 0 10px 10px 0;
  position: relative;
  text-decoration: none;
  color: var(--text);
  transition: background 150ms ease, border-left-color 150ms ease;
}
.recent-save-row::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
}

.recent-save-row:hover,
.recent-save-row:focus-within{
  background: var(--surface);
  border-left-color: var(--tier-accent);
}

.recent-save-side{
  position: relative;   /* anchors .recent-save-thumb-play overlay */
  flex-shrink: 0;
  width: 44px;
  height: 59px;
}

.recent-save-thumb{
  display: block;
  width: 44px;
  height: 59px;
  aspect-ratio: 3 / 4;   /* match the Library row thumb (44×59 portrait) */
  border-radius: 4px;
  overflow: hidden;
  background: rgba(0,0,0,0.18);
  text-decoration: none;
}

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

/* Recent Saves play triangle overlay */
.recent-save-thumb-play{
  position: absolute;
  top: 0;
  left: 0;
  width: 44px;
  height: 59px;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--motion-action-fade) ease;
  z-index: 1;
}

.recent-save-thumb-play-btn{
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: auto;
  text-decoration: none;
  line-height: 1;
  padding: 6px;
}

.recent-save-thumb-play-btn .play-triangle{
  width: 13px;
  height: auto;
  fill: #fff;
  filter: drop-shadow(0 1px 3px rgba(0,0,0,0.5));
}

.recent-save-row:hover .recent-save-thumb-play,
.recent-save-row:focus-within .recent-save-thumb-play{
  opacity: 1;
}

/* The body is the positioned container (mirrors compact Library's
   .library-link): a content link in flow + an absolutely-positioned action
   row pinned to the thumb's bottom edge. min-height: 59px (the thumb height)
   keeps the action row bottom-aligned even when the summary is short or
   absent (e.g. failed saves) so the icons never float up into the title. */
.recent-save-body{
  flex: 1;
  min-width: 0;
  position: relative;
  min-height: 59px;
}

.recent-save-content-link{
  display: block;
  text-decoration: none;
  color: inherit;
}

.recent-save-title{
  font-size: var(--type-preview-size);
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.recent-save-desc{
  font-size: var(--type-meta-size);
  line-height: 1.12;
  /* Body copy in --text-preview — the shared mid-ground body-copy colour
     (brighter than meta-muted, a clear step under the --text title). */
  color: var(--text-preview);
  margin-top: 2px;
  /* 2 lines at rest (matches the compact Library row's hover-reveal: more
     body copy shows on first inspection, no action icons competing). */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  max-height: calc(var(--type-meta-size) * 1.12 * 2);
}

/* Failed-state body message keeps the muted chrome colour (not the bright body
   copy) — it's a diagnostic, not content. */
.recent-save-fail-msg{
  color: var(--muted);
}

/* Action row — channels + delete + status pill, mirroring the compact
   Library row's .library-bottom-actions. Pins are intentionally absent
   (Library-only sort feature). Absolutely pinned to the body's bottom edge
   and hover-revealed so the row reads as a clean title + summary at rest. */
.recent-save-actions{
  position: absolute;
  left: 0;
  bottom: 0;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 4px;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-action-fade, 140ms) ease;
  --icon-btn-size: 28px;
  --icon-glyph-size: 15px;
}

/* On hover/focus: summary hard-clamps to ONE line (the 2nd line gives way) so
   at least one line of body copy stays visible beneath the fading-in action
   row — no text bleeds behind the icons (row backgrounds are translucent, so
   an opaque mask isn't available; clipping the lower line is the
   theme-agnostic equivalent). */
/* Reveal trigger: :hover (desktop) OR .actions-revealed (JS adds it on the
   first tap on touch — same tap-to-reveal handler as the Library rows). */
.recent-save-row:hover .recent-save-desc,
.recent-save-row:focus-within .recent-save-desc,
.recent-save-row.actions-revealed .recent-save-desc{
  -webkit-line-clamp: 1;
  line-clamp: 1;
  max-height: calc(var(--type-meta-size) * 1.12);
}
.recent-save-row:hover .recent-save-actions,
.recent-save-row:focus-within .recent-save-actions,
.recent-save-row.actions-revealed .recent-save-actions{
  opacity: 1;
  pointer-events: auto;
}

/* In-flight saves: surface the status pill AT REST so processing is obvious
   without hovering. Only the pill shows — the action buttons are hidden
   throughout the processing state (not toggled on hover, so no layout shift).
   When the save reaches Done/Failed the row drops .is-processing and the
   standard hover-only behaviour returns (excerpt at rest, actions on hover). */
.recent-save-row.is-processing .recent-save-actions{
  opacity: 1;
}
.recent-save-row.is-processing .recent-save-actions > :not(.status-pill){
  display: none;
}

/* ───────────────────────────────────────────────────────────────────────────
   Dashboard home redesign (2026-06-20): a compact HERO grid ADDED above the
   existing "Just saved" rows. The rows + channels are UNCHANGED from before this
   workstream — every rule below belongs to the brand-new .hero-grid / .save-hero
   block, so nothing shared (.recent-save-row, also used by search; .channel-card;
   the Library's .library-row-shell) is touched.
   ─────────────────────────────────────────────────────────────────────────── */

/* A COLUMN grid that contrasts with the row list below, kept at the list's SAME
   6px rhythm so the two areas stay graphically balanced as the viewport widens /
   narrows. Compact thumbs keep video-frame thumbnails crisp. The grid is 4-up only
   on genuinely WIDE screens — with --content-max-width 980px, 4 cards stay a
   comfortable ~210px+ only above ~900px viewport; below that it drops to 2x2 (held
   down to mobile, never 1-up, per the 2x2 intent). */
/* Hero grid + rows list as one stream, separated by 24px (2026-08-13, was 12px).
   12px was an in-between value: too large to read as the 6px inner rhythm, too
   small to read as a deliberate break, so it landed as neither.

   24px is not arbitrary — it is the interval already sitting between the last
   card and the next section's heading, so the two blocks separate on the rhythm
   the page is otherwise using. That interval is composed rather than declared:
   .dashboard-section's 12px margin-bottom plus the ~12px of centring above the
   heading text inside the 40px .dashboard-section-head row. Written as a literal
   because only half of it would track a token change; if the section rhythm is
   ever retuned, re-measure and match it here. */
.saves-stream{ display: flex; flex-direction: column; gap: 24px; }
.hero-grid{
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-2);  /* shared with .library-carousel — keep them equal */
}
@media (max-width: 900px){
  .hero-grid{ grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* Hero card chrome mirrors the "Just saved" rows: a MUTED accent edge at rest that
   lifts to the tier colour on hover/focus, the hairline ::after frame, and the
   0 12px 12px 0 radius. --tier-accent is inherited from the tier-* class on .hero-grid. */
.save-hero{
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 7px;
  background: var(--surface-2);
  border-left: 3px solid var(--stripe-rest);
  border-radius: 0 12px 12px 0;
  padding: 8px;
  min-width: 0;
  transition: background 150ms ease, border-left-color 150ms ease;
}
.save-hero::after{
  content: '';
  position: absolute;
  inset: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 12px 12px 0;
  pointer-events: none;
}
.save-hero:hover,
.save-hero:focus-within{
  background: var(--surface);
  border-left-color: var(--tier-accent);
}
/* Homepage "Just saved": the leading hero card leads lit at rest as a "start
   here" anchor (mirrors the Library carousel). Scoped to .saves-stream so the
   empty-state ghost preview (which reuses .save-hero chrome) is untouched. */
.saves-stream .save-hero:first-child{
  background: var(--surface);
  border-left-color: var(--tier-accent);
}
/* ...and it yields the moment the user engages ANY save in the stream — a hero
   card OR a row below it — so only one accent shows. When engagement ends the
   :has() stops matching and the untouched default lights the lead again. */
.saves-stream:has(.save-hero:hover, .recent-save-row:hover) .save-hero:first-child:not(:hover),
.saves-stream:has(.save-hero:focus-within, .recent-save-row:focus-within) .save-hero:first-child:not(:focus-within){
  background: var(--surface-2);
  border-left-color: var(--stripe-rest);
}
.save-hero__thumb{
  position: relative;
  display: block;
  border-radius: 8px;
  overflow: hidden;
  aspect-ratio: 16 / 9;
  background: var(--surface-2);
}
.save-hero__thumb-link{ position: absolute; inset: 0; display: block; }
.save-hero__thumb img{ width: 100%; height: 100%; object-fit: cover; display: block; }
/* "Open original" play — the SAME white play-triangle as the Library/list rows
   (recent-save-thumb-play-btn supplies the white fill + drop-shadow), centered and
   revealed only on hover/focus of the card. */
.save-hero__play{
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  opacity: 0; pointer-events: none;
  transition: opacity var(--motion-action-fade) ease;
  z-index: 1;
}
.save-hero:hover .save-hero__play,
.save-hero:focus-within .save-hero__play{ opacity: 1; }
.save-hero__play .recent-save-thumb-play-btn{ pointer-events: auto; }
.save-hero__play .play-triangle{ width: 20px; height: auto; }
/* flex:1 lets the body fill the equal-height card so the pills (margin-top:auto
   below) pin to the bottom edge — aligning the pill row across all cards. */
.save-hero__body{ display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1; }
.save-hero__title-link{ text-decoration: none; color: inherit; }
/* Headline runs slightly bigger than the body, and is ALWAYS shown in full (no
   clamp) — only the summary below is clamped. */
.save-hero__title{
  margin: 0; font-size: 0.78rem; line-height: 1.22; letter-spacing: -0.005em; color: var(--text);
}
/* Key points (heroes show these instead of a prose summary, to differ from the
   rows). The `.save-hero` prefix (0,3,0 on the li) beats the page-level `.main li`
   rule (0,1,1) that otherwise forces --type-body-size. Same body-copy size as the
   list rows (--type-meta-size); only the headline runs slightly bigger. */
.save-hero .save-hero__keypts{
  /* No inter-item gap: the line-height carries the rhythm so the bullets read at
     body-copy density (matching the detail page), not spread apart. */
  margin: 0; padding: 0; list-style: none; color: var(--text-preview);
  display: flex; flex-direction: column; gap: 0;
}
.save-hero .save-hero__keypts li{
  position: relative; padding-left: 13px;
  font-size: var(--type-meta-size); line-height: 1.2;
  /* each point clamped to 2 lines so the card stays compact (custom ::before
     bullet because -webkit-box display drops the native list marker). */
  display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.save-hero .save-hero__keypts li::before{
  content: "•"; position: absolute; left: 2px; top: 0; color: var(--text-meta);
}
/* Interest pills — the standard site .cat-pill-<slug> (full pillar colour, same
   design), scaled ~20% smaller for the compact hero card. */
.save-hero__tax{ display: flex; flex-wrap: wrap; gap: 5px; margin-top: auto; padding-top: 6px; }
.save-hero__tax .cat-pill{
  font-size: calc(var(--type-meta-size) * 0.8);
  padding: 1px 7px;
  min-height: 18px;
}

.dashboard-topic-list{
  gap: 6px;
}

/* "Things you like" heading: title + (sparse-mode) operator info popover grouped
   on the LEFT so the eye-icon stays pinned RIGHT via the section-head's
   space-between (the info no longer trails the eye and shifts it). */
.dashboard-head-titlegroup{
  display: flex;
  align-items: center;
  gap: 2px;
  min-width: 0;
}

.trending-accent-row{
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px 8px 12px;
  background: var(--surface-2);
  border-left: 3px solid var(--topic-accent);
  border-radius: 0 10px 10px 0;
  position: relative;
  text-decoration: none;
  color: var(--text);
  transition: background 150ms ease;
}
.trending-accent-row::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
}
.trending-accent-row:hover{
  background: var(--surface);
}

.trending-tag-name{
  font-size: 12px;
  font-weight: 500;
  /* Wide enough for the full master-pillar names (longest: "Personal Development",
     "Creative & Design") — telling the user what the bar represents matters more than
     a few px of bar length. Ellipsis stays only as a last-resort safety net. */
  width: 152px;
  flex-shrink: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
@media (max-width: 560px){
  /* On a phone the row is tighter; give the label what it needs, bar takes the rest. */
  .trending-tag-name{ width: 132px; }
}

.trending-bar-track{
  flex: 1;
  height: 4px;
  background: rgba(255,255,255,0.06);
  border-radius: 2px;
  overflow: hidden;
}

html[data-theme="light"] .trending-bar-track{
  background: rgba(0,0,0,0.06);
}

.trending-bar-fill{
  height: 100%;
  border-radius: 2px;
  background: var(--topic-accent);
}

.trending-accent-row .trending-count{
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  width: 24px;
  text-align: right;
  color: var(--muted);
  flex-shrink: 0;
}

/* Favourite creators card (metrics reflection surface) — WHO shapes your
   attention. Edge-accent OUTPUT row (per the edge-accent canon): thumb + a
   two-line text block (name+platform+count / lane+momentum) + proportion bar.
   Reuses the trending row's accent/hairline vocabulary, taller for two lines. */
/* TWO COLUMNS (2026-08-13). The rows now carry a bio and a divergence line, so a
   single full-width column left long, sparse rows with a lot of dead space to the
   right of short text. Two columns pull the content back into a readable measure
   and halve the vertical run of the card. auto-fit + a 300px floor collapses to
   one column on narrow viewports without a media query, so the phone layout is
   the same rule rather than a second one to keep in step. */
.metric-creator-list{
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 6px;
}
.metric-creator-row{
  display: flex;
  /* Centred, matching the channel-row triangles above it. This works now only
     because the body below has a FIXED line count (bio reserves 2 lines, the
     sub-line never wraps) — against a ragged body, centring floats the mark. */
  align-items: center;
  gap: 12px;
  /* Deliberately DEEPER than the channel rows above (a fixed-height scan list).
     These are a different kind of object — a reference you read, not a list you
     skim — and at near-identical heights they read as a broken variant of the
     channel row rather than something distinct. The depth comes from padding
     rather than a reserved third bio line, which would sit visibly empty since
     bios land at ~2 lines by design. */
  padding: 14px 14px 14px 12px;
  background: var(--surface-2);
  /* Edge stripe is MUTED at rest and lifts to the creator's assigned colour on
     hover — mirrors the channel-detail rows / Home channel cards (the
     trending-children mechanism). --topic-accent carries the per-creator colour. */
  border-left: 3px solid var(--stripe-rest);
  border-radius: 0 10px 10px 0;
  position: relative;
  transition: background 150ms ease, border-left-color 150ms ease;
  text-decoration: none;
  color: var(--text);
}
.metric-creator-row::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
}
.metric-creator-row:hover{ background: var(--surface); border-left-color: var(--topic-accent, var(--tier-accent)); }
/* The link is the row's CONTENT (avatar + body); the grip is its sibling, so the
   grip has no <a> ancestor and Firefox's long-press Link Preview cannot fire on it
   mid-drag. See the row comment in _favourite_creators_card.html. The link carries
   the avatar↔body gap; the row's own gap now sits between link and grip, which is
   the same 12px it always was — the layout is unchanged, only the nesting. */
.metric-creator-link{
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 12px;
  text-decoration: none;
  color: inherit;
}
/* Creator monogram — replaces the 3:4 card thumb (2026-08-13). CIRCULAR on
   purpose: every other image on the page is a 3:4 or 16:9 content rectangle, so a
   circle reads instantly as "a person, not a save" and the card stops looking like
   another row of saves. Initials + background are set inline from the creator's
   assigned palette colour, the same hue as their edge stripe and bar. */
.metric-creator-avatar{
  width: 36px;
  min-width: 36px;
  height: 36px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.02em;
  /* Optical: uppercase initials sit high in the line box, so nudge down to centre
     the visual mass rather than the box. */
  padding-top: 1px;
  user-select: none;
  transition: filter 150ms ease;
}
/* The row links out to the creator's profile; with the thumb's hover play-triangle
   gone, the monogram brightening is what keeps that affordance legible. */
.metric-creator-row:hover .metric-creator-avatar{ filter: brightness(1.25); }
/* Pillar glyph variant: inline SVG, sized in services/pillar_glyphs.py (24px at
   stroke 1.5). The icon carries its own optical centring, so drop the monogram's
   baseline nudge. font-size is gone with the webfont — do NOT reintroduce a size
   here, or it will fight the width/height the SVG carries and put the stroke back
   off the pixel grid. */
.metric-creator-avatar--pillar{ padding-top: 0; }
.metric-creator-avatar--pillar svg{ display: block; }
.metric-creator-body{
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.metric-creator-line{
  display: flex;
  align-items: center;
  gap: 6px;
}
.metric-creator-name{
  /* 12px to match .channel-list-name — the creator rows sit directly below the
     channel rows on this page, so the two name treatments have to agree or the
     page reads as two different systems stacked. */
  font-size: 12px;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.metric-creator-platform{
  display: inline-flex;
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  color: var(--muted);
}
.metric-creator-platform svg{ width: 100%; height: 100%; display: block; }

.metric-creator-sub{
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  min-width: 0;
  /* ONE line, always. It carries up to three short facts, any of which may be
     absent; letting it wrap put some cards onto an extra line and not others,
     which is what made the grid ragged. The recency chip is compact ("2w ago",
     not "saved 2 weeks ago") precisely so this fits — see _compact_age. */
  flex-wrap: nowrap;
  white-space: nowrap;
  overflow: hidden;
  min-height: 16px;
}

/* Revisits lead the sub-line and take the accent — they are the differentiated
   signal on this card (derived from in-product opens, which no external source
   can supply), so they should read first and brightest. */
.metric-creator-revisits{
  flex-shrink: 0;
  font-weight: 600;
  color: var(--topic-accent, var(--tier-accent));
}
.metric-creator-recency{
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.metric-creator-momentum{
  flex-shrink: 0;
  /* margin-left:auto dropped — it pinned momentum to the far right against the
     lane, but the sub-line is now a run of chips that should sit together and
     wrap as one group. */
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.02em;
}
/* Cadence is MUTED, including new/rising (2026-08-13). Revisits already carries
   the accent on this line, and two accent-coloured chips side by side read as two
   alarms rather than one signal — worst on a newly-saved creator, where "revisited"
   and "new" lit up together. Revisits keeps the colour because it is the
   differentiated signal; the arrow still marks a rise. */
.metric-creator-momentum--new,
.metric-creator-momentum--rising{ color: var(--text-meta); }

/* Reorder grip — mirrors .channel-list-grip. touch-action:none is the
   mobile-critical bit: dragging the grip must reorder, not scroll the page, and
   only the grip opts out of scrolling. The SVG is pointer-events:none so the
   button stays the stable gesture target. */
.metric-creator-grip{
  flex-shrink: 0;
  align-self: center;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 36px;
  /* NO negative margin here, unlike .channel-list-grip. That row is full-width so
     pulling the grip toward the edge is harmless; these sit in a GRID, and a grip
     pulled into the column gutter reads as belonging to the card on its right —
     you press what looks like this card's grip and its neighbour lifts, because it
     genuinely is the neighbour's grip. Keep it inside its own card. */
  padding: 0;
  background: none;
  border: none;
  color: var(--text-meta);
  cursor: grab;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  transition: color 150ms ease;
}
.metric-creator-grip:hover{ color: var(--text); }
.metric-creator-grip svg{ pointer-events: none; }
.metric-creator-row.is-dragging{
  opacity: 0.9;
  z-index: 3;
  box-shadow: var(--shadow-md);
}
.metric-creator-list.is-reordering{ user-select: none; }
.metric-creator-list.is-reordering .metric-creator-row{ transition: none; }

/* Bio: two lines max. The rewrite is capped at 90 chars so this rarely clamps,
   but the clamp guarantees a stable row height in a grid where neighbours must
   line up. */
.metric-creator-bio{
  font-size: 11px;
  line-height: 1.35;
  color: var(--text-preview);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  /* RESERVE both lines (2 * 1.35 * 11px). These rows sit in a grid, so a
     one-line bio next to a two-line bio made neighbouring cards different
     heights. Reserving the space is what makes every card read the same and
     break the same, and it is also what lets the avatar centre cleanly. */
  min-height: 29.7px;
}
/* The divergence is the card's payoff line, so it gets the accent and sits last
   before the bar — read the who, then the habit, then the surprise. */
.metric-creator-divergence{
  font-size: 10.5px;
  line-height: 1.35;
  color: var(--text-meta);
}
.metric-creator-divergence strong{
  color: var(--topic-accent, var(--tier-accent));
  font-weight: 650;
}
.metric-creator-momentum--steady{ color: var(--text-disabled, var(--muted)); }
/* Narrowed to leave the grip its own column at the row's right edge. */
.metric-creator-bar-track{
  max-width: calc(100% - 34px);
  height: 4px;
  background: rgba(255,255,255,0.06);
  border-radius: 2px;
  overflow: hidden;
  margin-top: 2px;
}
html[data-theme="light"] .metric-creator-bar-track{
  background: rgba(0,0,0,0.06);
}
.metric-creator-bar-fill{
  height: 100%;
  border-radius: 2px;
  background: var(--topic-accent, var(--tier-accent));
}

/* Favourite creators "how many to show" slider — chevron-revealed, mirroring the
   channels-sort chevron mechanism scoped to .creators-card. Rows past the chosen
   count get .is-hidden (server-side default + the inline localStorage controller). */
.metric-creator-row.is-hidden{ display: none; }
/* Channels count slider hides channel cards/rows beyond the chosen count (same
   mechanic, both views). */
.channel-card.is-hidden,
.channel-list-row.is-hidden{ display: none; }
/* The channels count control is a full-width row inside the flex-wrap sort body
   (so it sits on its own line below the sort chips). */
.channels-count-control{ flex-basis: 100%; }
/* Title + chevron coupling (chevron adjacent to the title, grouped left). Distinct
   class from .saves-heading-group to avoid colliding with the channels-sort group
   on the same page. */
/* "Your people" now sits BELOW the channels grid on /ui/channels — breathing room
   above it separates the two sections and stops the heading's chevron hover-circle
   colliding with the channels card / "Build a new channel" button above. */
.creators-card{ margin-top: 22px; }
.creators-heading-group{
  align-items: center;
  gap: 8px;
  flex-wrap: nowrap;
}
.creators-heading-group[data-saves-filter-toggle]{
  cursor: pointer;
}
.creators-count-chevron{
  transition: transform var(--motion-hover) ease, color var(--motion-hover) ease, background var(--motion-hover) ease;
}
.creators-count-chevron .ti{
  font-size: 18px;
  line-height: 1;
  transition: transform var(--motion-hover) ease;
}
.creators-card[data-filter-expanded="true"] .creators-count-chevron .ti{
  transform: rotate(180deg);
}
.creators-count-body{
  overflow: hidden;
  transition:
    max-height var(--motion-accordion) ease,
    opacity calc(var(--motion-accordion) - 40ms) ease;
  max-height: 60px;
  opacity: 1;
}
.creators-card[data-filter-expanded="false"] .creators-count-body{
  max-height: 0;
  opacity: 0;
  /* Absorb one of the two .dashboard-section gaps (8px) the collapsed 0-height
     body would otherwise leave between heading and list (mirrors the
     channels-sort-body absorption). */
  margin-bottom: -8px;
}
.creators-count-control,
.channels-count-control,
.library-count-control{
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 4px 2px 6px;
}
/* Sort-direction toggle sits BEFORE the slider (left); tints to the tier accent
   when the section is flipped to oldest-first. Its glyph is a touch larger than the
   sibling count glyphs so the control reads clearly. The bar keeps the shared
   .*-count-bar 220px cap so it matches the channels / creators sliders. */
.library-order-toggle{ flex-shrink: 0; }
.library-order-toggle .ti{ font-size: 18px; line-height: 1; }
.library-order-toggle.is-active{ color: var(--tier-accent); }
/* Oldest-first: reverse the visible row order (the slider windows from the other
   end in JS). Scoped to the row lists — a no-op elsewhere. */
.library-list--reversed{ flex-direction: column-reverse; }
.creators-count-text,
.channels-count-text,
.library-count-text{
  font-size: 12px;
  color: var(--text-meta);
  white-space: nowrap;
}
.creators-count-value,
.channels-count-value,
.library-count-value{
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
/* Progress-bar-styled slider — matches the settings-page usage bars
   (.account-plan-usage-track/-bar: 4px --surface-2 track, --tier-accent fill,
   pill radius, NO outer hairline border) + a FLAT handle. A transparent native
   range overlays the bar for drag / keyboard / a11y; JS drives fill + handle. */
.creators-count-bar,
.channels-count-bar,
.library-count-bar{
  position: relative;
  flex: 1;
  max-width: 220px;
  height: 4px;
  background: var(--surface-2);
  border-radius: 999px;
}
.creators-count-bar-fill,
.channels-count-bar-fill,
.library-count-bar-fill{
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  background: var(--tier-accent);
  border-radius: 999px;
}
.creators-count-bar-handle,
.channels-count-bar-handle,
.library-count-bar-handle{
  position: absolute;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 3px;
  height: 12px;
  background: var(--text);
  border-radius: 1px;   /* flat thin handle, no border */
  pointer-events: none;
}
.creators-count-slider,
.channels-count-slider,
.library-count-slider,
.library-picker-slider{
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 100%;
  height: 16px;
  margin: 0;
  opacity: 0;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
}
.creators-count-slider:focus,
.channels-count-slider:focus,
.library-count-slider:focus,
.library-picker-slider:focus{ outline: none; }
.creators-count-slider::-webkit-slider-thumb,
.channels-count-slider::-webkit-slider-thumb,
.library-count-slider::-webkit-slider-thumb,
.library-picker-slider::-webkit-slider-thumb{
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
}
.creators-count-slider::-moz-range-thumb,
.channels-count-slider::-moz-range-thumb,
.library-count-slider::-moz-range-thumb,
.library-picker-slider::-moz-range-thumb{
  width: 16px;
  height: 16px;
  border: 0;
}

/* Per-section volume slider (Library Stage 1, 2026-06-21). The chevron +
   collapsible body mirror the Library filter chevron, but keyed off the
   section's OWN data-count-expanded so they never collide with the page
   filter's data-filter-expanded. The inner control/bar reuse the shared
   .*-count-* rules above. */
.library-count-chevron .ti{
  font-size: 18px;
  line-height: 1;
  transition: transform var(--motion-hover) ease;
}
.library-section[data-count-expanded="true"] .library-count-chevron .ti{
  transform: rotate(180deg);
}
.library-count-body{
  overflow: hidden;
  transition:
    max-height var(--motion-accordion) ease,
    opacity calc(var(--motion-accordion) - 40ms) ease;
  max-height: 60px;
  opacity: 1;
}
.library-section[data-count-expanded="false"] .library-count-body{
  max-height: 0;
  opacity: 0;
  /* Absorb one of the two .library-section gaps (8px) the collapsed 0-height
     body would otherwise add between heading and list (mirrors the filter
     body's absorption), keeping the title→first-row rhythm at 8px. */
  margin-bottom: -8px;
}
.library-row-wrap.is-hidden{ display: none; }

/* 'Older' month sub-groups — a quieter second level under the OLDER title; the
   months slider hides whole months beyond the chosen count. The wrapper gives
   months more separation (16px) than the 8px head→grid rhythm inside a month. */
.library-months{
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.library-month{
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.library-month.is-hidden{ display: none; }
.library-month-head{
  /* Hosts the density toggle beside the month label — in line with the results
     it governs, rather than up in the section head far from them. Sized to the
     icon button so the label sits on the same baseline as the control. */
  min-height: var(--icon-btn-size);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}
.library-month-label{
  color: var(--text-meta);
}

@media (prefers-reduced-motion: reduce){
  .library-count-chevron .ti,
  .library-count-body{
    transition: none;
  }
}

/* Recent-section horizontal carousel (Library Stage 2, 2026-06-21). Today /
   This week browse as a scroll-snap strip of compact glance cards; the vertical
   depth (and full row actions) stay in the lists below + on the detail page.
   The negative margin lets the first/last card sit flush to the section edges
   while the 2px padding keeps focus rings from clipping. */
.library-carousel{
  display: flex;
  /* Same gap as the homepage hero grid (--space-2). The Library's top row and
     the dashboard's "Just saved" row are the same kind of object at the same
     glance size, so they should not sit on different rhythms. */
  gap: var(--space-2);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  padding: 2px 2px 8px;
  margin: 0 -2px;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  scrollbar-color: var(--surface-2) transparent;
}
.library-carousel::-webkit-scrollbar{ height: 6px; }
.library-carousel::-webkit-scrollbar-track{ background: transparent; }
.library-carousel::-webkit-scrollbar-thumb{ background: var(--surface-2); border-radius: 999px; }
.library-carousel-card{
  position: relative;
  flex: 0 0 auto;
  width: 178px;
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  gap: 6px;
  /* Same edge-accent chrome as the dashboard hero / behaviour cards: muted
     stripe at rest → tier accent on hover/focus, hairline ::after, right-rounded.
     --tier-accent is inherited from the tier-* class on <body>. */
  background: var(--surface-2);
  border-left: 3px solid var(--stripe-rest);
  border-radius: 0 12px 12px 0;
  padding: 8px;
  transition: background 150ms ease, border-left-color 150ms ease;
}
.library-carousel-card::after{
  content: '';
  position: absolute;
  inset: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 12px 12px 0;
  pointer-events: none;
}
/* Leading card leads lit (:first-child): on mobile there's no hover, so the
   first save carries the accent edge at rest as a "start here" anchor; hover/
   focus lights any card the same way. */
.library-carousel-card:hover,
.library-carousel-card:focus-within,
.library-carousel-card:first-child{
  background: var(--surface);
  border-left-color: var(--tier-accent);
}
/* The leading-card accent is a resting "start here" anchor, not a focus state.
   The moment the user engages ANY save anywhere in the library — a card in this
   row OR a row in another section (Earlier in August, a calendar month) — it
   yields so only the hovered/focused save carries the accent (one highlight,
   never two). Scoped to .library-sections (the wrapper of every section) so it
   reaches across sections, not just within the carousel. When engagement ends
   the :has() stops matching and the untouched default lights the lead again. */
.library-sections:has(.library-carousel-card:hover, .library-row-shell:hover) .library-carousel-card:first-child:not(:hover),
.library-sections:has(.library-carousel-card:focus-within, .library-row-shell:focus-within) .library-carousel-card:first-child:not(:focus-within){
  background: var(--surface-2);
  border-left-color: var(--stripe-rest);
}
.library-carousel-media{
  position: relative;
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface-2);
}
.library-carousel-thumb{
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  text-decoration: none;
  border: none;   /* drop the .media-thumb hairline — the card frame is the edge */
}
.library-carousel-thumb img{
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Override the row's 44×59 absolute play overlay so it covers the full-bleed
   card thumb; reveal on hover/focus (mirrors the row + dashboard hero). */
.library-carousel-play{
  inset: 0;
  width: auto;
  height: auto;
}
.library-carousel-media:hover .library-carousel-play,
.library-carousel-media:focus-within .library-carousel-play{
  opacity: 1;
}
.library-carousel-link{
  text-decoration: none;
  display: block;
}
.library-carousel-title{
  font-size: var(--type-meta-size);
  line-height: 1.3;
  color: var(--text);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.library-carousel-fail{
  font-size: var(--type-meta-size);
  color: var(--text-meta);
  margin-top: 2px;
}

/* Carousel edge fade — applied by JS (is-fade-start / is-fade-end) to whichever
   side still has scroll travel, so a partial card fades toward the remaining
   direction. Plain mask-image; both edges fade when scrolled mid-strip. */
[data-library-carousel].is-fade-end{
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 40px), transparent);
          mask-image: linear-gradient(to right, #000 calc(100% - 40px), transparent);
}
[data-library-carousel].is-fade-start{
  -webkit-mask-image: linear-gradient(to right, transparent, #000 40px);
          mask-image: linear-gradient(to right, transparent, #000 40px);
}
[data-library-carousel].is-fade-start.is-fade-end{
  -webkit-mask-image: linear-gradient(to right, transparent, #000 40px, #000 calc(100% - 40px), transparent);
          mask-image: linear-gradient(to right, transparent, #000 40px, #000 calc(100% - 40px), transparent);
}

/* Section title + its slider/navigator chevron cluster on the left (the chevron
   sits adjacent to the title, matching the top section's filter chevron). */
.library-section-title-group{
  min-width: 0;
}

/* 'Older' year-dropdown + month-slider navigator. Reuses the .library-count-*
   bar; adds the native year <select> + the month-name label. Slider first, year
   dropdown last, all on one horizontal line. */
.library-picker-control{
  flex-wrap: nowrap;
}
/* The month bar fills the row between the label and the year dropdown (the
   count slider's 220px cap suited a short 'Show N' control, but the Jan→Dec
   navigator wants the full width). Label + dropdown hold their size. */
.library-picker-control .library-count-bar{
  max-width: none;
  flex: 1 1 auto;
}
.library-picker-month-label{
  flex: 0 0 auto;
}
.library-picker-year{
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 3px 8px;
  cursor: pointer;
}
.library-picker-month-label{
  min-width: 4.5em;   /* hold width across month names so the bar doesn't jump */
}

/* 'Older' calendar — month tiles per year, each in a trending-palette hue
   (--cal-fill set inline). Months with saves are coloured + clickable; empty
   months dim. Clicking a tile reveals that month's saves below (see
   _library_older_calendar.html). */
.library-older{
  display: flex;
  flex-direction: column;
  gap: 12px;
}
/* Early-month heads-up filling the gap where "Earlier in {Month}" would sit, just
   above Previously. A quiet readable line (not a card): body-copy size + preview
   colour (matching the empty-state instructional-body treatment), full width so it
   aligns with the sections above/below rather than wrapping awkwardly narrow. */
.library-early-month-note{
  margin: 2px 0 10px;
  color: var(--text-preview);
  font-size: var(--type-body-size);
  line-height: 1.5;
}
/* Years browse horizontally — each year is a full-width scroll-snap page; the
   right edge fades (via data-library-carousel) to hint more years. */
.library-calendar-track{
  display: flex;
  gap: 12px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  scrollbar-color: var(--surface-2) transparent;
}
.library-calendar-track::-webkit-scrollbar{ height: 6px; }
.library-calendar-track::-webkit-scrollbar-track{ background: transparent; }
.library-calendar-track::-webkit-scrollbar-thumb{ background: var(--surface-2); border-radius: 999px; }
.library-calendar{
  flex: 0 0 100%;
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.library-calendar-year{
  font-size: var(--type-label-size);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: var(--type-meta-track);
  color: var(--text-meta);
}
.library-calendar-grid{
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 6px;
}
/* Edge-accent canon (matches the save rows / hero cards): a left stripe carries
   the month's DOMINANT-pillar colour persistently (semantic, not decorative) over
   a near-neutral body, right-rounded. Hover/active deepens the tint rather than
   washing the whole tile, so the archive never out-shouts the live content above. */
.library-cal-tile{
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: space-between;
  gap: 6px;
  min-height: 44px;
  padding: 6px 8px;
  /* Uses the SAME mechanism as the global .edge-accent: a real 3px border-left (crisp,
     always exactly 3px — an inset box-shadow antialiases at subpixel positions and reads a
     different thickness tile-to-tile) + an ::after hairline on top/right/bottom only (NO
     left border → it never mitres against the 3px accent). Kept mitre-free, now consistent. */
  position: relative;
  border: 0;
  border-left: 3px solid var(--cal-fill, transparent);
  border-radius: 0 8px 8px 0;
  background: color-mix(in srgb, var(--cal-fill) 7%, var(--surface-2));
  color: var(--text);
  cursor: pointer;
  text-align: left;
  font: inherit;
  transition: background 150ms ease;
}
.library-cal-tile::after{
  content: '';
  position: absolute;
  inset: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 8px 8px 0;
  pointer-events: none;
}
.library-cal-tile:hover,
.library-cal-tile.is-active{
  background: color-mix(in srgb, var(--cal-fill) 15%, var(--surface));
}
.library-cal-top{
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 6px;
}
.library-cal-name{
  font-size: var(--type-label-size);
  text-transform: uppercase;
  letter-spacing: var(--type-meta-track);
  color: var(--text-meta);
}
.library-cal-count{
  font-size: var(--type-meta-size);
  line-height: 1;
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  color: var(--cal-fill);
}
/* Magnitude bar — width scales with the month's share of the busiest month
   (--cal-intensity), so a glance reads which months were active. Floored so a
   single save still shows a sliver. */
.library-cal-mag{
  height: 3px;
  width: 100%;
  border-radius: 999px;
  background: color-mix(in srgb, var(--cal-fill) 16%, transparent);
  overflow: hidden;
}
.library-cal-mag-fill{
  display: block;
  height: 100%;
  width: max(10%, var(--cal-intensity, 0%));
  border-radius: 999px;
  background: var(--cal-fill);
}
.library-cal-tile.is-empty{
  /* No accent — just the uniform hairline ring (inset shadow, no mitre). */
  border-left: 0;
  border-radius: 8px;
  background: transparent;
  box-shadow: inset 0 0 0 0.5px var(--border);
  opacity: 0.4;
  cursor: default;
}
/* Empty tiles carry their own full hairline ring (above) — suppress the base
   3-sided ::after so it isn't doubled. */
.library-cal-tile.is-empty::after{ display: none; }
.library-cal-tile.is-empty .library-cal-count{ color: var(--text-meta); }
@media (max-width: 560px){
  .library-calendar{ grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.dashboard-topic-pill{
  color: var(--topic-fill);
  border-color: var(--topic-fill);
  background: transparent;
}

.category-pill{
  color: var(--topic-fill);
  /* Translucent filled treatment, matching the .tag-pill row/summary pills:
     the pill's own colour at ~10% over the card (color-mix of --topic-fill with
     transparent yields exactly the --tag-*-bg token values) + a soft 30% border.
     Reads less intrusive / more "expensive" than the old solid-border outline,
     and derives entirely from the --topic-fill the markup already sets — no
     extra token threading. Falls back to transparent on browsers without
     color-mix (graceful: the prior outline look). */
  border-color: color-mix(in srgb, var(--topic-fill) 30%, transparent);
  background: color-mix(in srgb, var(--topic-fill) 10%, transparent);
  /* Match the .tag-pill hairline (0.5px) — the .tag base is 1px, which read
     thicker than the row/summary pills we're matching. */
  border-width: 0.5px;
}
html[data-theme="light"] .category-pill{
  /* Light theme uses the heavier 15% fill (matches --tag-*-bg light values). */
  background: color-mix(in srgb, var(--topic-fill) 15%, transparent);
}

.filter-pill-active{
  background: #ffffff !important;
  border-color: #ffffff !important;
  color: #0f1620 !important;
}

html[data-theme="light"] .filter-pill-active{
  background: #112c4a !important;
  border-color: #112c4a !important;
  color: #ffffff !important;
}

.pill-more-btn{
  background: transparent;
  border-style: solid;
  cursor: pointer;
  color: var(--muted);
  border-color: var(--border);
}

.pill-more-btn:hover{
  color: var(--text);
  border-color: var(--muted);
}

/* Saves-card filter — chevron + accordion folded into the saves card heading.
   Replaces the standalone filter card. Both Library and Archive use the same
   pattern; behaviour is driven by data-filter-expanded on the .library-shell
   host. */

.saves-heading-group{
  align-items: center;
  gap: 8px;
  flex-wrap: nowrap;
}

/* When the heading group is wired as the delegated toggle wrapper (filters
   exist), the whole label region is clickable. Mouse/touch convenience —
   keyboard focus + ARIA state still live on the chevron button alone. */
.saves-heading-group[data-saves-filter-toggle]{
  cursor: pointer;
}

/* Brand slider-controls glyph (Library + Channels filter toggles). Sized a touch
   under the 18px icon system (14.4px ≈ 20% smaller); height:auto keeps the
   512x480 aspect (no squash). fill is currentColor, so it takes the .icon-btn
   muted colour and the accent on hover. */
.saves-filter-glyph{
  display: block;
  width: 14.4px;
  height: auto;
}

/* Count badge — tier-coloured pill, sized for inline placement next to chevron. */
.filter-count-badge{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 6px;
  border-radius: 999px;
  background: rgba(var(--tier-accent-rgb, 96, 165, 250), 0.20);
  color: var(--tier-accent, currentColor);
  font-size: var(--type-label-size);
  font-weight: 600;
  line-height: 1;
}

/* Saves-card filter chevron — canonical .icon-btn vocabulary. Inherits the
   40px circle, transparent rest, --btn-hover circle on hover, --text-meta
   rest colour, --text on hover from .icon-btn / .icon-muted. Only the
   transform-on-expand and glyph-size additions live here. */
.saves-filter-chevron{
  transition: transform var(--motion-hover) ease, color var(--motion-hover) ease, background var(--motion-hover) ease;
}
.saves-filter-chevron .ti{
  font-size: 18px;
  line-height: 1;
  transition: transform var(--motion-hover) ease;
}
/* The filter toggle now carries an adjustments glyph (not a chevron), so it must
   NOT flip on expand — only directional chevrons (e.g. .library-count-chevron) do. */
.library-shell[data-filter-expanded="true"] .saves-filter-chevron .ti{
  transform: none;
}

.saves-filter-body{
  overflow: hidden;
  transition:
    max-height var(--motion-accordion) ease,
    opacity calc(var(--motion-accordion) - 40ms) ease;
  max-height: 600px;
  opacity: 1;
}

.library-shell[data-filter-expanded="false"] .saves-filter-body{
  max-height: 0;
  opacity: 0;
  /* The collapsed (0-height) filter body is a flex child of .library-section
     (gap:8px) now (section-titled Library, 2026-06-21), so the 8px gap applies
     on BOTH sides → 16px header→list. Absorb 8px so the first section's
     title→first-row gap equals the 8px rhythm on the dashboard / channel detail
     (it previously read ~14px and looked larger than other surfaces). */
  margin-bottom: -8px;
}

/* Visual rhythm fix: the heading row is ~40px tall (density-toggle-driven) with
   the title text centred, producing ~12px of internal buffer below the text
   before the parent 5px flex gap — so heading→pills reads as ~17px of
   whitespace. The library-list below has no equivalent top buffer (row painted
   edge starts immediately at the parent gap), so pills→list reads as only
   ~5px. Push the list further down by ~12px when filter is expanded so the
   below-pills gap matches the above-pills gap.

   Heading→pills gap target is --space-6 (16px). The filter body is now a flex
   child of .library-section (gap:8px, 2026-06-21), so add (--space-6 − 8px) of
   margin-top to reach 16px, and 8px below so pills→list (8px gap + 8px) reads
   the same ~16px above and below the pills. */
.library-shell[data-filter-expanded="true"] .saves-filter-body{
  margin-top: calc(var(--space-6) - 8px);
  margin-bottom: 8px;
}

@media (prefers-reduced-motion: reduce){
  .saves-filter-chevron,
  .saves-filter-chevron .ti,
  .saves-filter-body{
    transition: none;
  }
}

/* Channels index sort affordance (Commit 3, Channels v1.1 polish).
   The chevron mirrors the Library filter chevron exactly — an icon-only
   .icon-btn beside the heading; the glyph rotates on the inner <i> off the
   card's data-filter-expanded state, same motion tokens. The options below
   are bordered lozenges (the .category-pill outline vocabulary) with NO
   background fill; the active dimension is marked by tier-accent border +
   text. Active dimension persists across navigation via cookie (server);
   panel open/closed via sessionStorage (base.html parameterised handler). */
.channels-sort-chevron{
  transition: transform var(--motion-hover) ease, color var(--motion-hover) ease, background var(--motion-hover) ease;
}
.channels-sort-chevron .ti{
  font-size: 18px;
  line-height: 1;
  transition: transform var(--motion-hover) ease;
}
.library-shell[data-filter-expanded="true"] .channels-sort-chevron .ti{
  transform: rotate(180deg);   /* directional chevron — it reveals a count slider */
}
.channels-sort-body{
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  overflow: hidden;
  transition:
    max-height var(--motion-accordion) ease,
    opacity calc(var(--motion-accordion) - 40ms) ease;
  max-height: 200px;
  opacity: 1;
}
.library-shell[data-filter-expanded="false"] .channels-sort-body{
  max-height: 0;
  opacity: 0;
  /* Same 2px absorption as the Library filter body (see .saves-filter-body
     collapsed rule): the collapsed 0-height sort body is still a flex item, so
     the .library-shell gap:5px applies on BOTH sides → 10px header→grid. Trim
     2px to land on the dashboard/library 8px, keeping the first channel card
     ("Music Production") at the same Y across all three surfaces. */
  margin-bottom: -2px;
}
/* Match the Library filter-body expand rhythm: bump the heading→panel gap
   to --space-6 (16px) effective when expanded. */
.library-shell[data-filter-expanded="true"] .channels-sort-body{
  margin-top: calc(var(--space-6) - 5px);
  margin-bottom: 12px;
}
/* Sort options — bordered lozenge, no background fill (mirrors the
   .category-pill outline treatment: 999px capsule, 1px border). */
/* Sort pills adopt the .cat-pill / .category-pill vocabulary (2026-06-06):
   hairline 0.5px border at 30% of the pill's colour + a 10% translucent fill,
   12px radius. At rest the "colour" is the muted grey (--text-meta), so the
   inactive pills read as neutral cat-pills. Hover resolves every pill to one
   consistent colour (--text). Active keeps the tier-accent, now with the same
   partial fill rather than a bare outline. */
.sort-option{
  display: inline-flex;
  align-items: center;
  padding: 2px 9px;
  min-height: 22px;
  border-radius: 12px;
  border: 0.5px solid color-mix(in srgb, var(--text-meta) 30%, transparent);
  background: color-mix(in srgb, var(--text-meta) 10%, transparent);
  font-size: var(--type-meta-size);
  color: var(--text-meta);
  text-decoration: none;
  transition: color var(--motion-hover) ease, border-color var(--motion-hover) ease,
    background var(--motion-hover) ease;
}
.sort-option:hover,
.sort-option:focus-visible{
  color: var(--text);
  border-color: color-mix(in srgb, var(--text) 30%, transparent);
  background: color-mix(in srgb, var(--text) 10%, transparent);
}
.sort-option--active{
  color: var(--tier-accent, var(--text));
  border-color: color-mix(in srgb, var(--tier-accent, var(--text)) 30%, transparent);
  background: color-mix(in srgb, var(--tier-accent, var(--text)) 10%, transparent);
  font-weight: 600;
}
@media (prefers-reduced-motion: reduce){
  .channels-sort-chevron,
  .channels-sort-chevron .ti,
  .channels-sort-body{
    transition: none;
  }
}

/* Filter card v2 — axis section labels (2026-05-26).
   When two filter axes share the card (channels + categories), each axis
   gets a small caps section label above its pill row. Section labels
   visually anchor each axis and disambiguate user-curated channels from
   AI-assigned categories. The .saves-filter-axis wrapper provides the
   vertical stack; the inner .row.dashboard-pill-cloud retains the
   horizontal pill flow. */
.saves-filter-axis{
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.saves-filter-axis + .saves-filter-axis{
  margin-top: 10px;
}
.saves-filter-section-label{
  /* Inherits caps + tracking + 10px / 500 from the global .section-label
     utility (line 592). Bright variant — visible content labels per the
     bright/meta-dim canon in CLAUDE.md (Visible content section labels
     bright). */
  color: var(--text);
}

.density-toggle{
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.density-btn{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--icon-btn-size);
  height: var(--icon-btn-size);
  min-width: var(--icon-btn-size);
  min-height: var(--icon-btn-size);
  padding: 0;
  border-radius: 50%;
  border: none;
  background: transparent;
  color: var(--text-meta);
  cursor: pointer;
  text-decoration: none;
  line-height: 1;
}

/* Density toggle is a below-the-nav, non-admin surface, so its active/hover
   accent follows the user's TIER colour (--tier-accent, set by the body
   tier-{tier} class) rather than the brand blue, which read at odds with the
   tier-themed page. Fallback to --accent-blue for any surface without a tier
   class. */
.density-btn:hover{
  background: var(--btn-hover);
  color: var(--tier-accent, var(--accent-blue));
}

.density-btn.is-active{
  background: transparent;
  color: var(--tier-accent, var(--accent-blue));
}

.density-btn.is-active:hover{
  background: var(--btn-hover);
  color: var(--tier-accent, var(--accent-blue));
}

.density-btn.is-locked{
  cursor: default;
  color: var(--text-meta);
}

.density-btn.is-locked:hover{
  background: var(--btn-hover);
  color: var(--text-meta);
}

.pictogram-line{
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  fill: none;
}


.dashboard-mix-track{
  width: 100%;
  height: 6px;
  border-radius: 999px;
  overflow: hidden;
  background: rgba(255,255,255,0.12);
  display: flex;
}

.dashboard-mix-segment{
  height: 100%;
  display: inline-block;
  background: #0698ea;
}

.dashboard-mix-segment:first-child{
  border-radius: 999px 0 0 999px;
}

.dashboard-mix-segment.is-audio{
  background: rgba(255,255,255,0.38);
}

.dashboard-mix-segment:last-child{
  border-radius: 0 999px 999px 0;
}

.dashboard-pill-audio{
  color: rgba(255,255,255,0.72);
  border-color: rgba(255,255,255,0.38);
  background: transparent;
}

html[data-theme="light"] .dashboard-mix-track{
  background: rgba(17, 44, 74, 0.18);
}

html[data-theme="light"] .dashboard-mix-segment{
  background: #0698ea;
}

html[data-theme="light"] .dashboard-mix-segment.is-audio{
  background: rgba(17, 44, 74, 0.28);
}

html[data-theme="light"] .dashboard-pill-audio{
  color: var(--text);
  border-color: rgba(17, 44, 74, 0.35);
  background: transparent;
}

.library-list .library-row-wrap:last-child .library-link-row{
  margin-bottom: 0;
}

.library-link{
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0;
}

.library-row-content{
  min-width: 0;
  flex: 1 1 auto;
  padding: 0;
  border-radius: 0;
  transition: none;
}

.library-failure-msg{
  color: var(--muted);
}

/* The list/card failed-reason line (macro failed_reason) renders TEXT ONLY — the honest
   reason. The RN code itself is shown by the row-action status pill (_status_pill), so the
   reason line doesn't duplicate it. (No inline pill here by design.) */

.library-delete-form-inline{
  display: flex;
  justify-content: center;
  width: auto;
}

.library-archive-form-inline{
  display: flex;
  justify-content: center;
  width: auto;
}

.library-row-pin-form{
  display: inline-flex;
  align-items: center;
}

.library-row-pin-form .icon-btn{
  width: var(--icon-btn-size);
  height: var(--icon-btn-size);
  min-width: var(--icon-btn-size);
  min-height: var(--icon-btn-size);
}

@media (max-width: 720px) {
  .dashboard-metrics-grid{
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .dashboard-metrics-grid.dashboard-behaviour-grid{
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .recent-saves-list{
    grid-template-columns: minmax(0, 1fr);
  }

  .item-detail-header{
    flex-direction: column;
    align-items: flex-start;
  }

  .item-detail-header .title-wrap{
    max-width: 100%;
  }

  .item-actions{
    width: 100%;
    justify-content: flex-start;
    gap: 10px;
    flex-wrap: nowrap;
  }

  .item-actions .icon-btn{
    flex: 0 0 40px;
  }

  .summary-status{
    flex-direction: column;
    align-items: flex-start;
    margin-left: 0;
    margin-top: 8px;
    width: 100%;
  }

  .summary-chip{
    white-space: normal;
    max-width: 100%;
    width: fit-content;
  }

  .source-mechanics{
    flex-direction: row;
    align-items: center;
    margin-top: 0;
    gap: 6px;
    row-gap: 6px;
    column-gap: 6px;
  }

  .source-mechanics .summary-chip{
    white-space: nowrap;
    width: auto;
  }

  .item-detail-page .status-actions-row{
    flex-direction: row;
    align-items: center;
    flex-wrap: nowrap;
    gap: 6px;
  }

  .item-detail-page .status-line{
    width: auto;
    white-space: nowrap;
  }

  .item-detail-page .item-actions{
    width: auto;
    /* Uniform 4px gap at mobile widths — matches the intra-group gap so the
       action icons read evenly spaced (no inter-group differential). */
    gap: 4px;
    flex-wrap: nowrap;
  }

  .item-detail-page .item-actions-group{
    gap: 4px;
  }

  .item-detail-page .item-actions .icon-btn{
    width: 34px;
    height: 34px;
    min-width: 34px;
    min-height: 34px;
    flex: 0 0 34px;
  }

  .item-detail-page .summary-status{
    flex-direction: row;
    align-items: center;
    margin-top: 8px;
    width: 100%;
    flex-wrap: wrap;
    gap: 6px;
  }
}


.btn-equal{
  min-width: 110px;
  text-align: center;
}

/* ------------------------------------------------------------------
   New save page (/ui/items/new) — design language v1
   Section order top to bottom:
     1. Process button (tier-coloured when ready, grey when disabled)
     2. Add a Save card (plain card; paste input INSIDE carries the tier stripe)
     3. Connection container (plain card containing status row + disconnect)
   The two accent stripes (paste input + status row) sit at identical
   x-coordinates so they form a single vertical line down the page.
   ------------------------------------------------------------------ */

/* "Add a Save" — FLATTENED (full-width surfaces, 2026-06-11): no card container,
   so the heading + input row + helper sit flush-left like the other flattened
   pages, and the input row / connection row span the same width as the full-width
   Process button. The inner input row carries its OWN edge accent + hairline, so
   the outer card is redundant (canon: inner items self-styled → flatten). */
/* Flattened (full-width canon) — scoped under .new-item-page so the (0,2,0)
   specificity beats the @media(max-width:560px) .card{padding:12px;border-radius:10px}
   override (0,1,0, later source). Without the page scope the mobile .card rule
   re-padded the card 12px, so the edge-accent rows stopped sitting flush-left
   at ≤560px. */
.new-item-page .new-item-form-card{
  border: 0;
  border-radius: 0;
  background: transparent;
  padding: 0;
}

/* Paste-input row — asymmetric accent stripe, tier-coloured on focus.
   Stripe's x-coordinate must match the connection container's status row
   stripe so they form a clean vertical line. Both use the same .card padding
   (16px) on their parent, and both sit flush-left within that padding. */
.new-item-input-row{
  position: relative;
  border: 0;
  border-left: 3px solid var(--stripe-rest);
  border-radius: 0 10px 10px 0;
  background: var(--card-rest);
  transition: border-left-color 140ms ease, background 140ms ease;
}
.new-item-input-row::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
}
.new-item-input-row:focus-within{
  border-left-color: var(--tier-accent, var(--callout-accent));
}
.new-item-input-row input#source_url{
  width: 100%;
  border: 0;
  background: transparent;
  border-radius: 0;
  /* 52px tall to match the Process button + IG connect button + the Settings
     connection row (the input vertically centres its own text; padding is
     horizontal only). */
  min-height: 52px;
  padding: 0 72px 0 11px;
  outline: none;
  box-shadow: none;
  color: var(--text);
  position: relative;
  z-index: 1;
}
.new-item-input-row input#source_url:focus{
  border-color: transparent;
  box-shadow: none;
}
.new-item-input-row input#source_url::placeholder{
  color: var(--text-meta);
}
/* The stripe carries the readiness signal — suppress the global .input-ready
   border/box-shadow inside the input row so it doesn't double-paint. */
.new-item-input-row input.input-ready{
  border-color: transparent;
  box-shadow: none;
}

/* Browse icon inside the paste input row — second of two affordances on the
   input's right side (clear × sits to its left when the input has a value).
   Both share the .file-trigger vocabulary so the hover treatment matches
   every other secondary icon button on the page. Absolutely positioned so
   it doesn't disturb the input's text flow. */
.new-item-page .file-trigger{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 4px 8px;
  min-height: auto;
  min-width: 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text-meta);
  cursor: pointer;
  transition: color 140ms ease, background 140ms ease;
  text-decoration: none;
}
.new-item-page .file-trigger svg{
  width: 18px;
  height: 18px;
  display: block;
}
.new-item-page .file-trigger:hover,
.new-item-page .file-trigger:focus-visible{
  background: var(--btn-hover);
  color: var(--callout-accent);
  outline: none;
}
.new-item-page .new-item-url-browse{
  position: absolute;
  top: 0;
  bottom: 0;
  right: 6px;
  margin-block: auto;
  width: 28px;
  height: 28px;
  padding: 0;
  z-index: 2;
  /* Muted at rest → tier colour on hover (the SVG strokes currentColor; the page
     carries the tier class on <body> so --tier-accent resolves). */
  color: var(--text-meta);
  transition: color 140ms ease;
}
.new-item-page .new-item-url-browse:hover,
.new-item-page .new-item-url-browse:focus-visible{
  color: var(--tier-accent);
}
.new-item-page .file-name{
  font-size: var(--type-body-size);
  color: var(--text-meta);
  line-height: 1.3;
}
.new-item-page .file-name[hidden]{
  display: none;
}

/* Helper / error copy — body typography (matches the rest of the page) */
.new-item-page .helper-copy{
  font-size: var(--type-body-size);
  color: var(--text-meta);
  line-height: 1.3;
  min-height: 1.3em;
}
.new-item-page .helper-copy-error{
  color: var(--pill-failed-fg);
}
.new-item-page .helper-copy-error-link{
  color: var(--pill-failed-fg);
  text-decoration: underline;
}

/* Process button — primary CTA. Disabled (plain grey) until ready, then tier-coloured.
   Tier-accent is sourced from body.tier-{curator|analyst|navigator}; Curator is the
   fallback for free/unauthenticated users (set on body in new_item.html). */
.new-item-page #process-btn{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  min-height: 52px;
  /* Same outline-border vocabulary as other buttons (.channels-add-card etc.).
     1px is held across every state below so the tier-fill ready state doesn't
     shift layout — only the border COLOUR changes. */
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text-meta);
  font-size: var(--type-body-size);
  font-weight: 500;
  cursor: pointer;
  transition: background 140ms ease, color 140ms ease;
  margin: 0;
}
.new-item-page #process-btn:hover{
  /* Neutral (no valid URL yet): constant muted fill, label brightens to white — the
     same hover as the recovery buttons. Once a valid URL arms it (.cta-ready) it turns
     brand blue instead (below). */
  background: var(--surface-2);
  color: var(--text);
}
.new-item-page #process-btn.cta-ready{
  /* Brand blue (#0698ea), NOT tier-accent — Process is the "save" action, so it
     matches the canonical save affordance (the top-strip plus, also brand blue per
     canon). Aligns the save CTAs rather than personalising this one to tier. */
  background: #0698ea;
  border-color: transparent;
  color: #ffffff;
  cursor: pointer;
}
.new-item-page #process-btn.cta-ready:hover,
.new-item-page #process-btn.cta-ready:focus-visible{
  background: #0698ea;
  filter: brightness(0.92);
  outline: none;
}
.new-item-page #process-btn .process-glyph{ height: 15.3px; width: auto; flex-shrink: 0; }
/* Disabled HTML attribute wins over cta-ready (e.g. at plan limit) — stays grey */
.new-item-page #process-btn[disabled],
.new-item-page #process-btn.cta-ready[disabled]{
  background: var(--surface-2);
  color: var(--text-meta);
  cursor: not-allowed;
  opacity: 0.7;
  filter: none;
}

/* Connection container — plain card holding the status row + disconnect action.
   Same chrome as .new-item-form-card so stripes inside both line up. */
/* Connection card — FLATTENED too: the status row (connected/disconnected) carries
   its own edge accent + fill, so it spans full-width like the Process button with
   no outer card. */
/* Flattened + page-scoped (same reason as .new-item-form-card above). The card
   now leads the page ABOVE the form (IG connect at top, Process at bottom), so
   the 12px rhythm rides margin-BOTTOM, not margin-top. */
.new-item-page .new-item-connection-card{
  border: 0;
  border-radius: 0;
  background: transparent;
  padding: 0;
  /* 12px below — matches the homepage Initial-setup card's gap-after
     (.dashboard-section margin-bottom) so the connect button sits in the same
     rhythm across surfaces. */
  margin: 0 0 12px;
}

/* Channels onboarding banners (Commit G — Channels v1). Two mutually
   exclusive variants render at the top of /ui/items above the Library
   card: .channels-banner--intro (post-migration informational) and
   .channels-banner--nudge (ongoing "5+ saves zero channels" nudge).
   Both share the canonical stripe-row geometry — border-left 3px +
   ::after hairlines + border-radius 0 10px 10px 0 — mirroring
   .new-item-status-row. Signature blue (var(--callout-accent)) + 6%
   blue tint (var(--callout-bg)) communicates informational, not
   action-needed (which would be amber). */
.channels-banner{
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 10px 14px 10px 11px;
  margin-bottom: 12px;
  border: 0;
  border-left: 3px solid var(--callout-accent);
  border-radius: 0 10px 10px 0;
  background: var(--callout-bg);
  color: var(--text);
  font-size: var(--type-body-size);
  line-height: 1.4;
}
.channels-banner::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
}
.channels-banner-body{
  flex: 1;
  min-width: 0;
  position: relative;
  z-index: 1;
}
.channels-banner-title{
  font-weight: 500;
  margin-right: 4px;
}
.channels-banner-link{
  color: var(--callout-accent);
  text-decoration: none;
  margin-left: 4px;
  transition: color var(--motion-hover) ease;
}
.channels-banner-link:hover,
.channels-banner-link:focus-visible{
  color: var(--text);
}
.channels-banner-dismiss-form{
  flex-shrink: 0;
  margin: 0;
  position: relative;
  z-index: 1;
}
.channels-banner-dismiss{
  /* Inherits .icon-btn .icon-btn-sm .icon-muted vocabulary;
     no per-banner sizing override needed. */
}

/* ------------------------------------------------------------------
   Shared edge-accent surface — state-driven "you're at a limit" surfaces.
   Skeleton mirrors .channels-banner: 3px square-left edge + ::after carrying
   the three 0.5px hairlines + asymmetric 0 10px 10px 0 radius. The modifier
   sets edge colour + tint; edge + tint always travel together so they're one
   modifier, not two. Consumed by the Library at-limit banner (Commit 1) and
   the channels at-cap add-card (Commit 2) so the at-cap moment reads
   identically across surfaces.
     --amber  = at-cap / at-limit conversion moment. Tint + edge literally
                match the tier-strip at-limit pill (rgba(251,191,36,0.12) tint,
                #FBBF24 / --pill-processing-fg edge).
     --muted  = neutral / empty / normal. Rest-edge grey (--stripe-rest), no
                tint. (Used by the channels add-card's empty/normal states.)
   ------------------------------------------------------------------ */
.edge-accent{
  position: relative;
  border: 0;
  border-left: 3px solid var(--stripe-rest);
  border-radius: 0 10px 10px 0;
  transition: background var(--motion-hover) ease,
              border-left-color var(--motion-hover) ease;
}
.edge-accent::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
}
.edge-accent--amber{
  border-left-color: var(--pill-processing-fg);
  background: rgba(251, 191, 36, 0.12);
}
.edge-accent--muted{
  border-left-color: var(--stripe-rest);
  background: transparent;
}

/* (Library + account at-limit banner families removed — folded into the
   unified .limit-banner family defined above, near the channels add-card.) */

/* Status row inside connection container — traffic-light stripe + 6% bg tint */
.new-item-status-row{
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 10px 14px 10px 11px;
  border: 0;
  border-left: 3px solid var(--stripe-rest);
  border-radius: 0 10px 10px 0;
  color: inherit;
  text-align: left;
  font: inherit;
  transition: background 140ms ease, border-left-color 140ms ease;
}
/* On the account Connection card, the status row matches the other account
   rows' height (2026-06-08); scoped so the new-item page row is unaffected. */
.account-connection-card .new-item-status-row{
  min-height: 52px;
}
/* The IG connect button is height-matched ACROSS surfaces (2026-06-11): it uses
   the same 52px reference as the account Connection card row, so the affordance
   reads as one persistent control wherever it appears (the dashboard status TILE
   is the deliberate exception — it's a status display, not a connect button).
   Vertical padding → 0 so the centred content sits in 52px. */
.new-item-page .new-item-status-row{
  min-height: 52px;
  padding-top: 0;
  padding-bottom: 0;
}
.new-item-status-row::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
}
.new-item-status-row-ok{
  background: rgba(74, 222, 128, 0.06);
  border-left-color: var(--pill-done-fg);
}
.new-item-status-row-off{
  background: rgba(248, 113, 113, 0.06);
  border-left-color: var(--pill-failed-fg);
}
.new-item-status-row-trigger{
  cursor: pointer;
}
.new-item-status-row-trigger:hover,
.new-item-status-row-trigger:focus-visible{
  background: rgba(248, 113, 113, 0.10);
  outline: none;
}
.new-item-status-dot{
  font-size: 16px;
  line-height: 1;
  color: var(--pill-done-fg);
  flex-shrink: 0;
  position: relative;
  z-index: 1;
}
.new-item-status-icon{
  font-size: 16px;
  line-height: 1;
  color: var(--pill-failed-fg);
  flex-shrink: 0;
  position: relative;
  z-index: 1;
}
.new-item-status-label{
  font-size: var(--type-body-size);
  font-weight: 400;
  line-height: 1.2;
  position: relative;
  z-index: 1;
}
.new-item-status-row-ok .new-item-status-label{
  color: var(--pill-done-fg);
}
.new-item-status-row-off .new-item-status-label{
  color: var(--pill-failed-fg);
}

/* Disconnect — secondary destructive action sitting inline on the right of the
   status row (mirrors the dashboard health card's inline-action pattern). The
   row's green tint is the surface; the button stays transparent in both rest
   and hover so the tint reads through. State is communicated by text colour
   alone (dim grey → red on hover). */
.new-item-disconnect-form{
  margin: 0;
  margin-left: auto;
  display: flex;
  align-items: center;
  position: relative;
  z-index: 1;
}
.new-item-disconnect-btn{
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text-meta);
  cursor: pointer;
  transition: color 140ms ease;
  font-size: var(--type-body-size);
  font-weight: 500;
  min-height: auto;
  min-width: 0;
}
.new-item-disconnect-btn .ti{
  font-size: 16px;
  line-height: 1;
}
/* Brand disconnect glyph (ig_disconnect_icon) — inherits the button's
   currentColor (muted → red on hover). 15px to sit a touch under the label. */
.ig-disconnect-glyph{
  width: 15px;
  height: 15px;
  display: block;
  flex-shrink: 0;
}
.new-item-disconnect-btn:hover,
.new-item-disconnect-btn:focus-visible{
  background: transparent;
  color: var(--pill-failed-fg);
  outline: none;
}

.new-item-page input::placeholder{
  color: var(--text-meta);
}

.new-item-url-input-wrap{
  position: relative;
}

/* Danger-styled confirmation modal OK button (added 2026-05-17).
   Triggered by setting style: 'danger' on a CONFIRM_CONFIG entry. */
#confirm-modal-ok.is-danger{
  background: var(--pill-failed-fg);
  border-color: var(--pill-failed-fg);
  color: #ffffff;
}
#confirm-modal-ok.is-danger:hover,
#confirm-modal-ok.is-danger:focus-visible{
  background: var(--pill-failed-fg);
  filter: brightness(0.92);
  outline: none;
}

/* Confirm modal extra-html slot (added Commit 2 of tier-switch admin bypass,
   2026-05-27). Hidden by default via [hidden] attribute; populated via
   innerHTML when the trigger declares data-confirm-extra-html. First
   consumer: the reset-trial checkbox on Curator-target admin tier switches.
   Future affordances (Channels v1.1 drag-and-merge name field, etc.) reuse
   this same slot. .confirm-modal-checkbox-row styles the canonical
   checkbox-with-label inline control: small gap between control and label
   text, meta-dim colour so it reads as a secondary affordance below the
   primary body copy. */
/* Tier-switch confirm modal — structured spec rows (Library saves · Channels ·
   Price [· Plus]) showing from→to, plus a caveat paragraph on downgrades.
   Cloned from a <template> into the confirm-modal body. */
.confirm-spec{
  display: flex;
  flex-direction: column;
  gap: 7px;
  margin: 6px 0 12px;
}
/* Divider under the spec block ONLY when a note/caveat follows (downgrades);
   pure upgrades end on the spec, no dangling rule. */
.confirm-spec:not(:last-child){
  padding-bottom: 12px;
  border-bottom: 0.5px solid var(--border);
}
.confirm-spec-row{
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
.confirm-spec-label{
  font-size: var(--type-meta-size);
  color: var(--text-meta);
  white-space: nowrap;
}
.confirm-spec-vals{
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
}
.confirm-spec-from{ color: var(--text-meta); }
.confirm-spec-arrow{ color: var(--text-meta); }
/* The new value is the offering: bold + the target tier accent (the .tier-{x}
   class rides the .confirm-spec container, so --tier-accent resolves locally).
   Falls back to bright --text if the accent is undefined. */
.confirm-spec-to{
  font-weight: 600;
  color: var(--tier-accent, var(--text));
}
.confirm-spec-note{
  font-size: var(--type-meta-size);
  color: var(--text-preview);
  line-height: 1.4;
  margin: 0 0 8px;
}
.confirm-caveat{
  font-size: var(--type-meta-size);
  color: var(--text-meta);
  line-height: 1.4;
  margin: 0;
}
#confirm-modal-extra-html{
  /* Flex column so multiple checkbox rows stack vertically with even
     spacing instead of flowing inline (the two-checkbox tier-switch case). */
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 4px;
}
#confirm-modal-extra-html[hidden]{
  display: none;
}
.confirm-modal-checkbox-row{
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--type-body-size);
  color: var(--text);
  cursor: pointer;
}
.confirm-modal-checkbox-row input[type="checkbox"]{
  cursor: pointer;
}

.new-item-url-clear{
  position: absolute;
  top: 0;
  bottom: 0;
  right: 40px;
  margin-block: auto;
  /* Sit ABOVE the input (#source_url is z-index:1). Without this the input
     paints over the clear button and intercepts its clicks — the X read as
     "not working" (the browse button only worked because it carries z-index:2). */
  z-index: 2;
  width: 22px;
  height: 22px;
  min-width: 22px;
  min-height: 22px;
  border: 0;
  border-radius: 999px;
  padding: 0 !important;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1 !important;
  background: transparent;
  color: var(--muted);
  font-size: 14px !important;
  font-weight: 600;
  transform: none;
  opacity: 0;
  pointer-events: none;
  box-sizing: border-box;
}

.new-item-url-clear.visible{
  opacity: 1;
  pointer-events: auto;
}

.new-item-url-clear:hover{
  background: var(--btn-hover);
  color: var(--text);
}

/* Mobile overrides: X-AXIS ONLY (column stacking, widths, side padding, font-size).
   Y-axis properties (gap/row-gap, padding-top/bottom, height, margin-top/bottom)
   MUST live in base rules so vertical rhythm is consistent across viewports. */
@media (max-width: 560px){
  :root{
    --icon-btn-size: 34px;
    --icon-glyph-size: 15px;
    --type-display-size: clamp(1.9rem, 7.2vw, 2.45rem);
    --type-heading-size: 0.8rem;
  }

  .top{
    gap: 8px;
    padding: 12px var(--layout-inset-mobile);
  }

  .brand{
    min-width: 0;
    flex: 1 1 auto;
  }

  .brand-logo{
    height: 18px;
  }

  .brand-tagline{
    display: none;
  }

  .nav{
    gap: 2px;
    flex: 0 0 auto;
    flex-wrap: nowrap;
  }

  .icon-btn{
    width: var(--icon-btn-size);
    height: var(--icon-btn-size);
    min-width: var(--icon-btn-size);
    min-height: var(--icon-btn-size);
    padding: 0;
  }

  .icon{
    width: var(--icon-glyph-size);
    height: var(--icon-glyph-size);
  }

  .foot{
    grid-template-columns: auto 1fr auto;
    gap: 8px;
    padding: 12px var(--layout-inset-mobile);
  }

  .foot-center{
    align-items: center;
    justify-self: stretch;
  }

  .foot-right{
    gap: 4px;
  }

  .main{
    padding: 24px var(--layout-inset-mobile) 14px;
  }

  .header{
    gap: 8px;
    margin-bottom: 10px;
  }

  .row{
    gap: 8px;
  }

  .card{
    padding: 12px;
    border-radius: 10px;
  }

  .page-title,
  h1{
    font-size: var(--type-display-size);
    line-height: var(--type-display-line);
    letter-spacing: var(--type-display-track);
  }

  .status-line{
    font-size: 0.66rem;
  }

  .auth-title{
    font-size: var(--type-card-title-size);
    line-height: var(--type-card-title-line);
    letter-spacing: var(--type-card-title-track);
    text-transform: uppercase;
    font-weight: 400;
  }

  .summary-chip{
    font-size: var(--type-meta-size);
    padding: 2px var(--space-3) 2px 22px;
    min-height: 22px;
  }

  .title-with-thumb{
    gap: 10px;
  }

  .page-title{
    font-size: clamp(1.42rem, 6.4vw, 1.82rem);
    line-height: 1.05;
    letter-spacing: -0.018em;
  }

  .title-thumb{
    width: auto;
    height: calc(clamp(1.62rem, 7.2vw, 2.08rem) * 1.05 * 3);
    max-height: 98px;
    aspect-ratio: 3 / 4;
  }

  .item-actions{
    width: auto;
    justify-content: flex-start;
    gap: 8px;
    flex-wrap: nowrap;
    margin-top: 0;
  }

  .item-actions .icon-btn{
    flex: 0 0 var(--icon-btn-size);
  }

  .status-actions-row{
    flex-direction: row;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
  }

  .status-actions-row .status-line{
    width: auto;
  }

  .summary-status{
    flex-direction: row;
    align-items: center;
    margin-left: 0;
    margin-top: 8px;
    width: 100%;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-3);
  }

  .item-detail-page .title-thumb{
    height: calc(clamp(1.42rem, 6.4vw, 1.82rem) * 1.05 * 3);
    max-height: 98px;
  }

  .plan-limits-grid{
    grid-template-columns: 1fr;
  }

  .dashboard-metric-tile{
    padding: 8px;
  }

  .dashboard-metric-tile strong{
    font-size: var(--type-body-size);
  }

  .related-link{
    width: calc(100% + 24px);
    margin-inline: -12px;
    padding: 12px;
    border-radius: 10px;
  }

  .related-list{
    padding: 0;
  }

  .library-title{
    display: block;
    width: 100%;
  }

  /* New-item-page mobile overrides — X-AXIS ONLY.
     The form-card / connection-card are FLATTENED (no border/radius/padding) so
     the inner edge-accent rows (.new-item-input-row / .new-item-status-row) bleed
     to the column edge; their 0 10px 10px 0 stripe geometry is a global design
     language token that does NOT change with viewport width. The flatten is
     page-scoped to beat the @media .card override (see .new-item-page
     .new-item-form-card). Do not add mobile rules that touch
     .new-item-input-row { border-* }, .new-item-status-row { border-* }, or any
     ::before / ::after stripe pseudos for this page. */
  .new-item-page .title-wrap{
    max-width: 100%;
  }

  input,
  textarea,
  select{
    padding: 9px 10px;
    border-radius: 9px;
  }

  button{
    padding: 8px 11px;
  }

  .new-item-url-clear{
    width: 22px !important;
    height: 22px !important;
    min-width: 22px !important;
    min-height: 22px !important;
    padding: 0 !important;
    line-height: 1 !important;
    border-radius: 999px !important;
  }
}

/* Reduced motion — scope to row hover only; preserves functional animations (e.g. status spinner) */
@media (prefers-reduced-motion: reduce){
  .library-row-shell,
  .library-row-shell *,
  .library-excerpt{
    transition: none !important;
  }
}

/* /ui/trending — dedicated trending page */
.trending-page-sub{
  margin: 0;
}
.trending-page-list{
  gap: 6px;
}

/* Trending List/Map view switch + the interest treemap (Map view). */
[data-trending-view="list"] .trending-map-view{ display: none; }
[data-trending-view="map"] .trending-list-view{ display: none; }
/* Control swap: Map shows [Treemap][List] (no density); List shows
   [Treemap][density] (List button hidden) — max 3 icons, only one ever
   tier-highlighted. */
[data-trending-view="map"] .trending-density{ display: none; }
[data-trending-view="list"] .trending-list-btn{ display: none; }
.trending-treemap{
  position: relative;
  width: 100%;
  height: 360px;
  /* Match the app card surface (cohesion): same radius + hairline border. Gapless
     + sharp tiles, so the corner tiles simply clip to this rounded block — no
     mismatched notches. */
  border: 0.5px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
}
@media (max-width: 560px){ .trending-treemap{ height: 300px; } }
/* Compact variant for the dashboard "Things you like" card (shorter than the
   full /ui/trending page). On the (flat) dashboard it wears the standard
   accent-edge chrome — persistent tier stripe + right-rounded + hairline — so it
   reads as part of the dash. (--tier-accent comes from <body class="tier-…">.) */
.trending-treemap--compact{
  height: 220px;
  border: 0;
  border-left: 3px solid var(--tier-accent);
  border-radius: 0 12px 12px 0;
}
.trending-treemap--compact::after{
  content: '';
  position: absolute;
  inset: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 12px 12px 0;
  pointer-events: none;
}
/* Each tile = a pillar, area ∝ count, in its fixed pillar colour (var(--c)) with
   a WinDirStat-style 3D cushion (light top-left → dark bottom-right). No hairline
   borders — the gaps separate tiles. Positioned by the squarify JS. */
.treemap-tile{
  position: absolute;
  border-radius: 0;   /* sharp tiles → gapless flush layout (cushion separates) */
  overflow: hidden;
  cursor: pointer;
  text-decoration: none;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 8px 10px;
  color: #fff;
  /* Gentle cushion (dialled down) — a faint top-left lift + bottom-right falloff
     over the pillar colour, just enough to avoid a flat swatch. */
  background:
    radial-gradient(135% 135% at 28% 22%, rgba(255,255,255,.18), rgba(255,255,255,0) 46%),
    radial-gradient(150% 150% at 76% 86%, rgba(0,0,0,.22), rgba(0,0,0,0) 54%),
    var(--c);
  transition: filter 140ms ease;
}
/* Hover = brightness only, no border. (Keyboard focus keeps a ring for a11y.) */
.treemap-tile:hover{
  filter: brightness(1.08);
  z-index: 2;
}
.treemap-tile:focus-visible{
  filter: brightness(1.08);
  outline: 2px solid rgba(255,255,255,.6);
  outline-offset: -2px;
  z-index: 2;
}
/* Dashboard (compact) variant only: dial the tiles down ~16% brightness + a touch
   of saturation, so the treemap sits IN the muted dashboard rather than out-
   shouting the status/compass cards. The /ui/trending hero stays full-strength.
   Hover composes ON TOP of the dim (chained filters multiply) so a dashboard tile
   never snaps back to full brightness on hover. */
.trending-treemap--compact .treemap-tile{
  filter: brightness(.84) saturate(.9);
}
.trending-treemap--compact .treemap-tile:hover,
.trending-treemap--compact .treemap-tile:focus-visible{
  filter: brightness(.84) saturate(.9) brightness(1.08);
}
/* Label = name only (no count — the AREA conveys magnitude; count is on hover/
   title). Light-on-tile, no drop shadow (the solid pillar colour carries the
   contrast); clamps so it never spills a small tile. */
.treemap-nm{
  font-size: 0.72rem;   /* equal across all tiles (no per-size shrink) */
  font-weight: 600;
  line-height: 1.15;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  /* Break at spaces first; when a single long word must split, HYPHENATE it
     ("Produc-/tivity", not "Productivit/y"). hyphens:auto needs <html lang>. */
  overflow-wrap: break-word;
  word-break: normal;
  -webkit-hyphens: auto;
          hyphens: auto;
}
.treemap-tile.sm{ padding: 4px 6px; }
/* Narrow-but-tall tile → rotate the name 90° (reads bottom-to-top); ellipsis if
   it still overflows the tile height ("Personal Dev…"). */
.treemap-tile.vert{ padding: 8px 6px; }
.treemap-tile.vert .treemap-nm{
  display: block;
  -webkit-line-clamp: unset;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-height: 100%;
}
.treemap-tile.tiny .treemap-nm{ display: none; }

/* ── Timeline scrubber (/ui/trending Map view; below the treemap) ───────────
   Drag/play the playhead and the treemap tweens through cumulative snapshots.
   Tile position/size transitions ONLY while playing (resize + drag stay instant,
   so the static map and window resizes never animate). */
.trending-treemap--animating .treemap-tile{
  transition: left 300ms ease, top 300ms ease, width 300ms ease, height 300ms ease, opacity 300ms ease, filter 140ms ease;
}
.treemap-tile--empty{ opacity: 0; pointer-events: none; }   /* a category not yet present */
.treemap-nm{ transition: opacity 200ms ease; }              /* labels fade across size tiers */

.treemap-scrub{ display: flex; align-items: center; gap: 12px; margin-top: 12px; padding: 0 2px; }
.treemap-scrub-play{
  flex: 0 0 auto; width: 30px; height: 30px; border-radius: 50%;
  border: 1px solid var(--border); background: var(--surface); color: var(--text);
  display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
.treemap-scrub-play:hover{ background: var(--btn-hover); border-color: var(--tier-accent, #0698ea); color: var(--tier-accent, #0698ea); }
.treemap-scrub-ic{ display: inline-flex; align-items: center; justify-content: center; }
.treemap-scrub-tri{ width: 11px; height: 11px; }       /* the ReelNotes brand triangle */
.treemap-scrub-ic--play{ margin-left: 1px; }            /* optical-centre the triangle */
.treemap-scrub-ic--pause{ display: none; }
.treemap-scrub-pause{ width: 12px; height: 12px; }   /* two-bar pause, sized to read */
.treemap-scrub.is-playing .treemap-scrub-ic--play{ display: none; }
.treemap-scrub.is-playing .treemap-scrub-ic--pause{ display: inline-flex; }
/* Slider = the canonical count-slider control: 4px --surface-2 track, --tier-accent
   fill, flat handle, transparent native range overlay for drag/keyboard/a11y. */
.treemap-scrub-bar{ position: relative; flex: 1 1 auto; height: 4px; background: var(--surface-2); border-radius: 999px; }
.treemap-scrub-bar-fill{ position: absolute; top: 0; left: 0; bottom: 0; background: var(--tier-accent); border-radius: 999px; }
.treemap-scrub-bar-handle{ position: absolute; top: 50%; transform: translate(-50%, -50%); width: 3px; height: 12px; background: var(--text); border-radius: 1px; pointer-events: none; }
/* While playing, the fill + handle ease at the step cadence -> a progress bar
   filling smoothly month to month. Drag stays instant (transition only .is-playing). */
.treemap-scrub.is-playing .treemap-scrub-bar-fill{ transition: width var(--scrub-step, 300ms) linear; }
.treemap-scrub.is-playing .treemap-scrub-bar-handle{ transition: left var(--scrub-step, 300ms) linear; }
.treemap-scrub-slider{
  position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%);
  width: 100%; height: 16px; margin: 0; opacity: 0; cursor: pointer;
  -webkit-appearance: none; appearance: none; background: transparent;
}
.treemap-scrub-slider:focus{ outline: none; }
.treemap-scrub-slider::-webkit-slider-thumb{ -webkit-appearance: none; width: 16px; height: 16px; }
.treemap-scrub-slider::-moz-range-thumb{ width: 16px; height: 16px; border: 0; }
.treemap-scrub-date{
  flex: 0 0 auto; min-width: 62px; text-align: right;
  font-size: var(--type-meta-size); color: var(--text-meta); font-variant-numeric: tabular-nums;
}
@media (prefers-reduced-motion: reduce){
  .trending-treemap--animating .treemap-tile{ transition: filter 140ms ease; }
}

.trending-row-wrap{
  display: block;
}
button.trending-accent-row{
  appearance: none;
  -webkit-appearance: none;
  /* Reset <button> UA-default borders on the three sides where the ::after
     hairline takes over. The base .trending-accent-row rule's
     `border-left: 3px solid var(--topic-accent)` continues to apply. */
  border-top: 0;
  border-right: 0;
  border-bottom: 0;
  background: var(--surface-2);
  width: 100%;
  text-align: left;
  cursor: pointer;
  font: inherit;
  color: var(--text);
}
button.trending-accent-row:hover{
  background: var(--surface);
}
.trending-saves-panel{
  overflow: hidden;
  transition: max-height var(--motion-accordion, 220ms) ease;
  padding: 0 0 0 10px;
}
.trending-saves-panel[hidden]{
  display: none;
}
.trending-saves-panel:not([hidden]){
  padding-top: 8px;
  padding-bottom: 4px;
}
/* Child save rows light up with the parent topic's accent edge ON HOVER only
   (at rest they keep the default grey stripe, like every other row). The panel
   carries --topic-accent (set by JS on expand from the clicked topic row);
   the row-shell left stripe reads it on hover/focus, falling back to the tier
   accent when unset — so hovering a save shows which topic "owns" it. */
.trending-saves-panel .library-row-shell:hover,
.trending-saves-panel .library-row-shell:focus-within{
  border-left-color: var(--topic-accent, var(--tier-accent));
}
/* Channel detail save rows light up with THIS channel's accent edge ON HOVER —
   same mechanism + hover-only rule as the trending children above. The list
   carries --topic-accent (= the channel's colour, set inline from channel_colour);
   the row-shell left stripe reads it on hover/focus, so hovering a save shows
   which channel "owns" it (cross-surface channel identity, canon #13/#19). */
.channel-detail-list .library-row-shell:hover,
.channel-detail-list .library-row-shell:focus-within{
  border-left-color: var(--topic-accent, var(--tier-accent));
}
/* Heading→first-row gap: match the Library/dashboard 8px standard. The
   .library-shell stack gap is 5px, and channel detail has no collapsed filter
   body to carry the Library's +3px (.saves-filter-body) adjustment, so the bare
   gap read 5px — 3px tighter than the sibling retrieval surfaces. +3px brings the
   first row to the same Y. Content-state ONLY (the list renders only when there
   are saves; the empty/onboarding state has no .channel-detail-list, so its
   already-finessed rhythm is untouched). */
.channel-detail-list{ margin-top: 3px; }
.trending-saves-loading{
  padding: 12px 8px;
  font-size: var(--type-meta-size, 11px);
}
.trending-saves-empty{
  padding: 12px 8px;
  font-size: var(--type-meta-size, 11px);
}
.trending-empty-msg{
  padding: 4px 0;
}

@media (display-mode: standalone) {
}

/* ============================================================================
   Admin design language alignment
   ============================================================================
   Admin pages inherit consumer tokens (colours, borders, typography, motion)
   and inherit ornaments selectively (stripes appear on input fields and
   semantic action rows; metric tiles stay plain so the scannable grid
   doesn't get noisy).
   ============================================================================ */

/* Section header — primary 10px/500 caps label + optional right-aligned link */
.admin-section-head{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: var(--icon-btn-size);
}

/* Groups the section title + its info tip together on the LEFT of a section
   head, so the info icon sits adjacent to the title it describes. Any card
   action (Save / Manage) stays on the far right via the head's space-between. */
.admin-section-head-label{
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

/* Admin section labels — bright white. Aligns with the SUMMARY card label
   canon established on the detail page (visible content section labels are
   bright; demoted / diagnostic labels stay meta-dim). The admin operator
   surface labels are still primary navigation cues — they should read as
   content, not scaffolding. Caps + 0.08em letter-spacing + 10px / 500
   weight preserved; only the colour changes from text-meta → text.
   (Fix 5b, 2026-05-18 sixth session) */
.admin-section-head .admin-section-label{
  /* Normalised to the standard section-heading size (matches .card-heading on
     the dashboard / library / channels), 2026-06-08 — was the small 10px
     --type-label-size, which read undersized against the rest of the app. */
  font-size: var(--type-card-title-size);
  line-height: var(--type-card-title-line);
  letter-spacing: var(--type-card-title-track);
  font-weight: 400;
  text-transform: uppercase;
  color: var(--text);
}

/* Section action link — meta-dim, no underline, brightens on hover. Applies
   inside a section head AND the page head (the Plan Limits "Exit plan limits"
   link lives in .admin-page-head). */
.admin-section-head .admin-section-action,
.admin-page-head .admin-section-action{
  font-size: var(--type-meta-size);
  font-weight: 400;
  color: var(--text-meta);
  text-decoration: none;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  transition: color var(--motion-hover) ease;
}

.admin-section-head .admin-section-action:hover,
.admin-section-head .admin-section-action:focus-visible,
.admin-page-head .admin-section-action:hover,
.admin-page-head .admin-section-action:focus-visible{
  color: var(--text);
  text-decoration: none;
}

/* Sentence-case sub-label, distinct from primary uppercase label */
.admin-llm-provider-row{
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 6px 10px; border-radius: 8px; background: var(--surface-2);
  font-size: 0.86rem;
}
.admin-llm-provider-name{ color: var(--text); }
.admin-llm-provider-stat{ color: var(--text-meta); white-space: nowrap; }
/* LLM cost stats — reuse the metric-field pattern (label OUTSIDE the tile) so it
   matches the other economics panels; 3 edge-accent OUTPUT tiles on ONE row, equal
   height. The edge-accent treatment is re-asserted here so .admin-metric-tile's full
   border/radius (later in source) doesn't override the left-stripe look. */
.admin-llm-stats{ grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: stretch; }
.admin-llm-tile{
  flex: 1;
  /* figure + sub on ONE line (baseline-aligned, no wrap) so every tile is a single
     row → minimal, equal height across the three. */
  flex-direction: row;
  align-items: baseline;
  justify-content: center;
  flex-wrap: nowrap;
  gap: 6px;
  border: 0;
  border-left: 3px solid var(--accent, #0698ea);
  border-radius: 0 10px 10px 0;
}
.admin-llm-tile strong{ font-size: 1.2rem; line-height: 1.1; white-space: nowrap; }
.admin-llm-sub{ font-size: 0.78rem; color: var(--text-meta); white-space: nowrap; }
@media (max-width: 640px){ .admin-llm-stats{ grid-template-columns: 1fr; } }
.admin-section-subtitle{
  font-size: var(--type-label-size);
  font-weight: 400;
  color: var(--text-meta);
  margin: 0;
}

/* Neutral data pill — same shape as .status-pill, queued-state slate palette,
   text-only (no dot). Used wherever subscriber/economics data was previously
   wearing tag-rotation colours. */
.admin-data-pill{
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 9px;
  min-height: 22px;
  border-radius: 10px;
  border: 0;
  background: var(--pill-queued-bg);
  color: var(--pill-queued-fg);
  font-size: var(--type-meta-size);
  font-weight: 400;
  line-height: 1.1;
  white-space: nowrap;
  transition: filter var(--motion-hover) ease;
}
/* Read-only output pills get a slight neutral lift on hover so the surface
   feels alive/consistent with its interactive siblings — but NOT a colour or
   cursor change (these aren't clickable; tier colour is reserved for the
   tier chips, blue for genuine affordances). filter: brightness is paint-only
   (no layout shift) and theme-agnostic — each pill keeps its own colour,
   including the green .is-live pill, just lifted. */
.admin-data-pill:hover{
  filter: brightness(1.12);
}

.admin-data-pill .admin-data-pill-label{
  color: var(--text-meta);
}

.admin-data-pill .admin-data-pill-sep{
  color: var(--text-meta);
  margin: 0 2px;
}

.admin-data-pill .admin-data-pill-value{
  color: var(--text);
  font-weight: 500;
}

/* Live status pill — semantic green for "X live" subscribers indicator only */
.admin-data-pill.is-live{
  background: var(--pill-done-bg);
  color: var(--pill-done-fg);
}
.admin-data-pill.is-live .admin-data-pill-value{
  color: var(--pill-done-fg);
}

/* Inline mid-prose value — bare bright text replacing pill-wrapped figures
   embedded in sentences (e.g. "Recommended monthly price is £0.94/month"). */
.admin-inline-value{
  color: var(--text);
  font-weight: 500;
}

/* Tier chip — a neutral slate PILL (same shape as .admin-data-pill) for
   intrinsically tier-scoped admin data: Plan Economics figures (prices,
   cost-parity, subs-to-cover) and the Subscribers tier counts. The accent-edge
   stripe is reserved for LARGE containers (metric tiles, input rows); at this
   small inline scale a 3px stripe reads as a thin sliver, so these use a pill
   instead. On hover the whole pill turns its tier colour (tier-tinted bg +
   tier-accent text), resolved per chip from its .tier-{curator|analyst|
   navigator} class. Non-tier admin data (cost components, the green "live"
   count) keeps the plain .admin-data-pill (no tier hover). */
.admin-tier-chip{
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 9px;
  min-height: 22px;
  border: 0;
  border-radius: 10px;
  background: var(--pill-queued-bg);
  color: var(--pill-queued-fg);
  font-size: var(--type-meta-size);
  font-weight: 400;
  line-height: 1.1;
  white-space: nowrap;
  transition: background var(--motion-hover) ease, color var(--motion-hover) ease;
}
.admin-tier-chip:hover{
  background: rgba(var(--tier-accent-rgb), 0.18);
  color: var(--tier-accent);
}
.admin-tier-chip .admin-tier-chip-label{
  color: var(--text-meta);
}
.admin-tier-chip .admin-tier-chip-sep{
  color: var(--text-meta);
  margin: 0 2px;
}
.admin-tier-chip .admin-tier-chip-value{
  color: var(--text);
  font-weight: 500;
}
/* Whole pill (label + separator + value) takes the tier colour on hover. */
.admin-tier-chip:hover .admin-tier-chip-label,
.admin-tier-chip:hover .admin-tier-chip-sep,
.admin-tier-chip:hover .admin-tier-chip-value{
  color: var(--tier-accent);
}

/* Subscribers progress bars — one per tier, each a click-to-manage target.
   Adjacent on desktop (flex row, each bar flexing to share width), STACKED on
   mobile — the same layout-axis reshape the MY PLAN dual bars use. Tier fill
   colour resolves from the per-bar .tier-{tier} class via --tier-accent, reusing
   .account-plan-usage-track / .account-plan-usage-bar. */
.admin-subs-progress-row{
  display: flex;
  flex-direction: row;
  gap: 16px;
}
.admin-subs-bar{
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 6px 8px;
  margin: 0;
  border: 0.5px solid transparent;
  border-radius: 8px;
  background: transparent;
  text-align: left;
  cursor: pointer;
  transition: background var(--motion-hover) ease, border-color var(--motion-hover) ease;
}
.admin-subs-bar:hover,
.admin-subs-bar:focus-visible{
  background: var(--surface-2);
  border-color: var(--border);
}
.admin-subs-bar-head{
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
.admin-subs-bar-tier{
  font-size: var(--type-meta-size);
  font-weight: 400;
  color: var(--text-meta);
  transition: color var(--motion-hover) ease;
}
.admin-subs-bar-count{
  font-size: 1rem;
  font-weight: 500;
  color: var(--text);
  transition: color var(--motion-hover) ease;
}
/* Count brightens to the tier colour on hover — paint-only, no layout shift. */
.admin-subs-bar:hover .admin-subs-bar-count,
.admin-subs-bar:focus-visible .admin-subs-bar-count{
  color: var(--tier-accent);
}
@media (max-width: 560px){
  .admin-subs-progress-row{
    flex-direction: column;
    gap: 8px;
  }
}

/* Subscribers management modal — wider scrollable card, left-aligned content. */
.admin-subs-notice{
  font-size: var(--type-meta-size);
  font-weight: 400;
  margin: 0;
}
.admin-subs-notice--ok{ color: var(--pill-done-fg); }
.admin-subs-notice--err{ color: var(--pill-failed-fg); }
.subs-manage-card{
  width: min(680px, calc(100vw - 2rem));
  max-height: 80vh;
  overflow-y: auto;
  text-align: left;
}
.subs-manage-head{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 12px;
}
.subs-manage-head .modal-title{
  font-size: var(--type-body-size);
  font-weight: 500;
  color: var(--text);
}
/* Right-side header actions: CSV download + close, sharing the icon-btn circle. */
.subs-manage-head-actions{
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
/* Bulk-select toolbar: controls apply to every ticked user in the group. Sticky
   to the top of the scrolling modal so it stays reachable down a long roster. */
.subs-bulk-toolbar{
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 12px;
  padding: 10px 0;
  margin-bottom: 4px;
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface-chrome);
  border-bottom: 0.5px solid var(--border);
}
.subs-bulk-selectall{
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--type-meta-size);
  color: var(--text);
  white-space: nowrap;
  cursor: pointer;
}
.subs-bulk-count{
  font-size: var(--type-meta-size);
  color: var(--text-meta);
  min-width: 64px;
}
.subs-bulk-actions{
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-left: auto;
}
/* Bulk action buttons read inert until at least one user is selected. */
.subs-bulk-actions button[disabled]{
  opacity: 0.45;
  cursor: not-allowed;
}
.subs-bulk-list{
  list-style: none;
  margin: 0;
  padding: 0;
}
.subs-bulk-row{
  padding: 8px 0;
  border-top: 0.5px solid var(--border);
}
.subs-bulk-list > .subs-bulk-row:first-child{
  border-top: 0;
}
.subs-bulk-check-label{
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  min-width: 0;
}
.subs-member-info{
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.subs-member-email{
  font-size: var(--type-body-size);
  font-weight: 500;
  color: var(--text);
  word-break: break-word;
}
.subs-member-meta{
  font-size: var(--type-meta-size);
  color: var(--text-meta);
}
.subs-member-field{
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.subs-member-field-label{
  font-size: var(--type-meta-size);
  color: var(--text-meta);
}
.subs-member-select{
  font-size: var(--type-meta-size);
  padding: 4px 8px;
}
.subs-member-reset{
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--type-meta-size);
  color: var(--text-meta);
  white-space: nowrap;
  cursor: pointer;
}
.subs-member-apply{
  font-size: var(--type-meta-size);
  font-weight: 500;
  color: #fff;
  background: var(--callout-accent);
  border: 0;
  border-radius: 8px;
  padding: 5px 12px;
  cursor: pointer;
  transition: filter var(--motion-hover) ease;
}
.subs-member-apply:hover{ filter: brightness(1.08); }
.subs-member-apply:active{ filter: brightness(1.14); }
.subs-member-bonus-input{
  width: 68px;
  font-size: var(--type-meta-size);
  padding: 4px 8px;
}
/* "Gift" reuses the Apply shape but reads as a secondary (bordered) action so
   the primary tier "Apply" stays dominant. */
.subs-member-gift{
  font-size: var(--type-meta-size);
  font-weight: 500;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 5px 12px;
  cursor: pointer;
  transition: filter var(--motion-hover) ease, border-color var(--motion-hover) ease;
}
.subs-member-gift:hover{ border-color: var(--callout-accent); filter: brightness(1.05); }
/* "+N bonus" badge in the member meta line — positive grant, success-tinted. */
.subs-member-bonus-tag{
  color: var(--pill-done-fg);
  font-weight: 500;
}
/* Deactivate: muted at rest, danger-tinted on hover (paint-only per the hover
   canon — border reserved transparent at rest so only colour shifts). */
.subs-member-deactivate{
  font-size: var(--type-meta-size);
  font-weight: 500;
  color: var(--text-meta);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 5px 12px;
  cursor: pointer;
  transition: color var(--motion-hover) ease, border-color var(--motion-hover) ease, background var(--motion-hover) ease;
}
.subs-member-deactivate:hover{
  color: var(--pill-failed-fg);
  border-color: var(--pill-failed-fg);
  background: rgba(248, 113, 113, 0.10);
}
.subs-member-empty{
  font-size: var(--type-meta-size);
  color: var(--text-meta);
  margin: 0;
  padding: 8px 0;
}

/* Metric tile — plain card vocabulary, hover-lightens.
   Uses existing .dashboard-metric-tile structure but with admin-specific
   typography overrides for value bright at 16px/500 and label meta at 11px. */
.admin-metric-tile{
  background: var(--surface);
  border: 0.5px solid var(--border);
  border-radius: 10px;
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  transition: background var(--motion-hover) ease;
}

.admin-metric-tile:hover{
  background: var(--surface-2);
}

.admin-metric-tile > span,
.admin-metric-tile .admin-metric-label{
  font-size: var(--type-meta-size);
  font-weight: 400;
  color: var(--text-meta);
  letter-spacing: 0;
  text-transform: none;
}

.admin-metric-tile > strong,
.admin-metric-tile .admin-metric-value{
  font-size: 1rem;
  font-weight: 500;
  color: var(--text);
}

.admin-metric-tile .admin-metric-sub{
  font-size: var(--type-meta-size);
  font-weight: 400;
  color: var(--text-meta);
}

/* Label-above layout for read-only metric tiles. Mirrors .admin-input-field
   (6px gap, meta-dim label) so every field on both admin pages reads the same:
   label → accent-edge box → optional sub-line. The .admin-metric-tile below
   now holds ONLY the value. (Trial-expired stat tiles keep their label INSIDE
   the tile via the .admin-metric-tile .admin-metric-label rule above — that
   consumer is unchanged.) */
.admin-metric-field{
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.admin-metric-field > .admin-metric-label{
  font-size: var(--type-meta-size);
  font-weight: 400;
  color: var(--text-meta);
}

/* Admin Economics: data tiles wear the muted accent-edge grammar so every
   container on the page (tiles + rate inputs + Save/Exit/Lock rows) shares
   one vocabulary. Neutral --stripe-rest stripe — identity-of-grammar, not
   state: unchanged on hover (the base hover-lightens still applies). Native
   top/right/bottom borders suppressed so the 3px border-left has nothing to
   mitre against at the square left corners; ::after carries the three 0.5px
   hairlines (canon stripe pattern). Scoped to .admin-econ-root so the
   trial-expired stat tiles (.trial-expired-stat-tile, tier-accent stripe)
   and any other .admin-metric-tile consumer are untouched — plan-limits also
   carries .admin-econ-root but renders no metric tiles, so only the economics
   tiles match. Reverses the earlier "metric tiles stay plain" call now that
   the accent-edge grammar is the page-wide standard. */
.admin-econ-root .admin-metric-tile{
  position: relative;
  border-top: none;
  border-right: none;
  border-bottom: none;
  border-left: 3px solid var(--stripe-rest);
  border-radius: 0 10px 10px 0;
  /* Both background AND edge animate on hover. This declaration overrides the
     base .admin-metric-tile transition (background only) by specificity, so it
     must restate background to keep the hover-lighten animating. */
  transition: background var(--motion-hover) ease, border-left-color var(--motion-hover) ease;
}
/* Hover affordance: the muted grey edge brightens to signature blue when the
   admin points at any accent-edge container — same blue as the input
   focus-within state, rolled out to hover panel-wide. The base
   .admin-metric-tile:hover still lightens the background. */
.admin-econ-root .admin-metric-tile:hover{
  border-left-color: var(--callout-accent);
}
.admin-econ-root .admin-metric-tile::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
}

/* Asymmetric accent edge stripe on numeric input rows.
   Matches .new-item-input-row geometry exactly so admin focus state reads
   the same as the new-save paste input. */
.admin-input-row{
  position: relative;
  border: 0;
  border-left: 3px solid var(--stripe-rest);
  border-radius: 0 10px 10px 0;
  background: var(--card-rest);
  transition: border-left-color var(--motion-hover) ease, background var(--motion-hover) ease;
}

.admin-input-row::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
}

/* Hover brightens the edge to signature blue (panel-wide hover affordance,
   matching the metric tiles). focus-within below keeps the same blue when the
   field is active; the per-tier :focus-within override on Plan Limits is more
   specific, so a focused tier input still glows its tier colour. */
.admin-input-row:hover{
  border-left-color: var(--callout-accent);
}

.admin-input-row:focus-within{
  border-left-color: var(--callout-accent);
}

/* Plan Limits page: per-tier focus stripe (Fix 6, 2026-05-18 sixth session).
   Tier-specific inputs (.admin-input-row--tier inside a .tier-{tier}
   wrapper) override the global blue focus stripe with the tier's own
   accent colour. Curator inputs glow violet, Analyst pink, Navigator
   indigo — the admin sees the tier identity reinforced while editing
   that section. Signature blue stays the canonical primary-action /
   commit colour on the page (the "Save plan limits" stripe row).

   The .tier-{tier} class on the wrapper propagates --tier-accent via
   the existing global tier-token rule (.tier-curator, .tier-analyst,
   .tier-navigator), so this single :focus-within rule resolves the
   correct colour per row.

   Note: admin remains tier-NEUTRAL by default — this exception applies
   only on Plan Limits because each input is, by definition, scoped to
   a specific tier the admin is editing. Other admin surfaces (cost
   snapshot, security) continue to use signature blue universally. */
.admin-input-field.tier-curator .admin-input-row--tier:focus-within,
.admin-input-field.tier-analyst .admin-input-row--tier:focus-within,
.admin-input-field.tier-navigator .admin-input-row--tier:focus-within{
  border-left-color: var(--tier-accent);
}

/* Tier-scoped inputs ALSO hover in their tier colour, not the generic blue.
   Establishes the panel-wide rule: tier-scoped accent-edge containers (these
   per-tier inputs + the Plan Economics / Subscribers tier chips) hover in
   tier colour; tier-neutral containers (metric tiles, rate inputs) hover blue.
   Higher specificity (0,4,0) beats the generic .admin-input-row:hover (0,2,0),
   so a hovered tier input glows its tier colour and the hover→focus colour
   no longer shifts. */
.admin-input-field.tier-curator .admin-input-row--tier:hover,
.admin-input-field.tier-analyst .admin-input-row--tier:hover,
.admin-input-field.tier-navigator .admin-input-row--tier:hover{
  border-left-color: var(--tier-accent);
}

.admin-input-row input[type="number"],
.admin-input-row input[type="password"],
.admin-input-row input[type="text"]{
  width: 100%;
  border: 0;
  background: transparent;
  border-radius: 0;
  padding: 8px 10px;
  outline: none;
  box-shadow: none;
  color: var(--text);
  font: inherit;
}

.admin-input-row input[type="number"]:focus,
.admin-input-row input[type="password"]:focus,
.admin-input-row input[type="text"]:focus{
  border-color: transparent;
  box-shadow: none;
}

/* Stack the input-row with its label above so labels line up with the
   form-row vocabulary used elsewhere. */
.admin-input-field{
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.admin-input-field > .admin-input-label{
  font-size: var(--type-meta-size);
  font-weight: 400;
  color: var(--text-meta);
}

.admin-input-field > .admin-input-sub{
  font-size: var(--type-meta-size);
  font-weight: 400;
  color: var(--text-meta);
}

/* Info-circle tooltip trigger — meta-dim icon that uses native title attribute
   as the MVP tooltip. Future: replace with a popover component. */
/* Info icon uses the canonical .icon-btn vocabulary (same circle + glyph size
   as the nav and section-head icons): --icon-btn-size circle, transparent at
   rest, --btn-hover background on hover, --icon-glyph-size (18px) glyph. */
.admin-info-tip{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--icon-btn-size);
  height: var(--icon-btn-size);
  min-width: var(--icon-btn-size);
  min-height: var(--icon-btn-size);
  border-radius: 50%;
  border: 0;
  background: transparent;
  color: var(--text-meta);
  cursor: pointer;
  padding: 0;
  vertical-align: middle;
  transition: color var(--motion-hover) ease, background var(--motion-hover) ease;
}

.admin-info-tip:hover,
.admin-info-tip:focus-visible{
  color: var(--text);
  background: var(--btn-hover);
  outline: none;
}

.admin-info-tip[aria-expanded="true"]{
  color: var(--callout-accent);
}

/* Brand info SVG (via info_icon macro). currentColor inherits the button's
   colour state (meta / hover-text / open-blue). Sized to the canonical glyph
   size, slightly inset (the circle fills its viewBox edge-to-edge, unlike a
   Tabler glyph) so its visual mass matches sibling icons. */
.admin-info-icon{
  width: var(--icon-glyph-size);
  height: var(--icon-glyph-size);
  display: block;
}

/* Click-toggle info popover — replaces the native `title` tooltip (which only
   showed on hover-and-wait and did nothing on click / touch). The wrap anchors
   the popover to the icon; it opens below and to the RIGHT of the icon (the
   icons sit next to their card titles on the left, so opening rightward keeps
   the full popover on screen, including on small viewports). Toggled by the
   delegated handler in base.html (click to open, click-outside / Escape). */
.admin-info-tip-wrap{
  position: relative;
  display: inline-flex;
}

.admin-info-popover{
  /* position:fixed + JS placement (base.html admin-info handler) so the popover
     CLAMPS to the viewport instead of overflowing off the right edge. Was
     absolute/left:0, which cropped when the icon sat mid-row or under the in-app
     `zoom` (where 100vw is layout px, not visual). Mirrors the ⋯ more-menu's
     zoom-compensated `place()`. JS sets left/top; these are the pre-paint
     fallback before measurement. */
  position: fixed;
  top: 0;
  left: 0;
  right: auto;
  z-index: 60;
  width: max-content;
  max-width: min(260px, calc(100vw - 2rem));
  padding: 9px 11px;
  /* Solid OPAQUE background so the tile/card behind never bleeds through.
     --surface-chrome is a true solid (#0d1926 dark / #FFFFFF light) — unlike
     --surface-2 (low-alpha overlay) or --stripe-rest (resolves to the rgba
     --border token), both of which are see-through. Bright text for contrast. */
  background: var(--surface-chrome);
  border: 0.5px solid var(--border);
  border-radius: 8px;
  box-shadow: var(--shadow-md);
  font-size: var(--type-meta-size);
  font-weight: 400;
  line-height: 1.4;
  color: var(--text);
  white-space: normal;
  text-align: left;
}

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

/* Page-level currency toggle — segmented control vocabulary with text labels.
   Active state: 20% alpha blue + bright blue text; inactive: transparent + slate. */
.admin-currency-toggle{
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.admin-currency-btn{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 28px;
  padding: 4px 10px;
  border-radius: 999px;
  border: 0;
  background: transparent;
  color: var(--text-meta);
  font-size: var(--type-meta-size);
  font-weight: 400;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--motion-hover) ease, color var(--motion-hover) ease;
}

.admin-currency-btn:hover{
  background: var(--btn-hover);
  color: var(--text);
  text-decoration: none;
}

.admin-currency-btn.is-active{
  background: rgba(33, 150, 243, 0.20);
  color: var(--callout-accent);
}

.admin-currency-btn.is-active:hover{
  background: rgba(33, 150, 243, 0.28);
  color: var(--callout-accent);
}

/* Page header — admin page title + page-level currency toggle */
.admin-page-head{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 4px;
  /* Match .admin-section-head so the title sits at a consistent height across
     admin pages whether or not the head carries pill controls (Economics lost
     its currency pills to the Cost Snapshot card; Analytics keeps a toggle). */
  min-height: var(--icon-btn-size);
}

.admin-page-head .admin-page-title{
  font-size: var(--type-label-size);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-meta);
}

/* Bottom action bar — stripe-bearing rows replace the saturated full-fill CTAs.
   Same asymmetric accent edge geometry as connection rows on /ui/items/new. */
.admin-action-row{
  position: relative;
  border: 0;
  border-left: 3px solid var(--callout-accent);
  border-radius: 0 10px 10px 0;
  padding: 10px 14px;
  background: rgba(33, 150, 243, 0.06);
  color: var(--callout-accent);
  font-size: var(--type-body-size);
  font-weight: 400;
  cursor: pointer;
  text-align: left;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  width: 100%;
  transition: background var(--motion-hover) ease;
}

.admin-action-row::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
}

.admin-action-row:hover{
  background: rgba(33, 150, 243, 0.10);
}

.admin-action-row.admin-action-row--lock{
  border-left-color: var(--pill-failed-fg);
  background: rgba(239, 68, 68, 0.06);
  color: var(--pill-failed-fg);
}

.admin-action-row.admin-action-row--lock:hover{
  background: rgba(239, 68, 68, 0.10);
}

/* Bottom action bar layout — two stripe rows side-by-side, stacked on narrow */
.admin-action-bar{
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-top: 4px;
}

.admin-action-bar > form{
  margin: 0;
}

@media (max-width: 560px){
  .admin-action-bar{
    grid-template-columns: 1fr;
  }
}

/* ─────────────────────────────────────────────────────────────────────────
   Trial-expired conversion page (/ui/trial-expired)
   Hybrid in-app moment: photo mosaic (personal identity) → calendar heatmap
   + theme proportion bar + behavioural stat tiles (analytical narrative)
   → CTAs. All rules scoped under .trial-expired-page so blast radius stays
   small.
   ─────────────────────────────────────────────────────────────────────── */

.trial-expired-page .trial-expired-card{
  padding: 16px;
}

/* Visible content section labels on this page are bright (var(--text)) —
   they announce primary content, not diagnostic / scaffolding metadata.
   Scoped via .trial-expired-section-label sibling class so the global
   .section-label utility keeps its meta-dim default for other surfaces.
   Caps + letter-spacing + 10px / 500 weight all inherited from the base
   .section-label rule; only the colour changes. */
.trial-expired-section-label{
  color: var(--text);
}

.trial-expired-eyebrow{
  /* Inherits brightness from .trial-expired-section-label + sizing /
     tracking / caps from the global .section-label utility. */
}

.trial-expired-headline{
  font-size: var(--type-detail-size);
  font-weight: 500;
  line-height: 1.2;
  color: var(--text);
  margin: 0;
}

.trial-expired-subline{
  font-size: var(--type-body-size);
  font-weight: 400;
  color: var(--text-preview);
  margin: 0;
  line-height: 1.4;
}

/* Photo mosaic — container-driven flex-wrap layout. Tiles share the row
   via flex: 1 1 80px (grow to fill / shrink down to the 80px floor) so
   the browser does the math: ~3-4 per row on narrow mobile, ~6-8 on
   tablet, all 18 in one row on desktop. No explicit breakpoints. Uniform-
   height tiles via aspect-ratio: 9 / 12 (portrait, matching Instagram
   Reel ratio); over-tall portraits crop via object-fit: cover. The
   max-width: 140px ceiling stops tiles getting absurdly wide on desktop
   when the user has only a few saves. */
.trial-expired-mosaic{
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  position: relative;
}

.trial-expired-mosaic-tile{
  position: relative;        /* anchors .title-thumb-play absolute overlay */
  display: block;
  flex: 1 1 80px;
  min-width: 80px;
  max-width: 140px;
  aspect-ratio: 9 / 12;
  border-radius: 8px;
  overflow: hidden;
  border: 0.5px solid var(--border);
  transform-origin: center;
  transition: transform 140ms ease, filter 140ms ease;
  text-decoration: none;
}

.trial-expired-mosaic-tile:hover,
.trial-expired-mosaic-tile:focus-visible{
  transform: scale(1.02);
  filter: brightness(1.06);
  text-decoration: none;
}

.trial-expired-mosaic-tile img{
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Persistent play triangle overlay on each mosaic tile — reuses the
   .title-thumb-play vocabulary (always-visible variant) with a smaller
   13px triangle proportional to the smaller tile size. Matches Recent
   Saves' smaller-thumb treatment. */
.trial-expired-mosaic-tile .title-thumb-play .play-triangle{
  width: 13px;
}

/* Sparse-state copy when < 3 thumbnailed saves. Renders in place of the
   mosaic; analytics sections below still render. */
.trial-expired-sparse{
  font-size: var(--type-body-size);
  color: var(--text-preview);
  margin: 0;
  text-align: center;
  padding: 16px 8px;
}

/* Zero-saves "fresh start" branch — replaces all analytics + mosaic. */
.trial-expired-fresh-start{
  font-size: var(--type-body-size);
  color: var(--text-preview);
  margin: 0;
  text-align: center;
  padding: 24px 8px;
}

/* Generic section wrapper used by heatmap + proportion bar. Stacks label
   above its content with a small consistent gap. */
.trial-expired-section{
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Calendar heatmap — 30-day grid (7 weekday rows × 5 week cols) plus
   day-of-week labels on the left and optional annotation on the right. */
.trial-expired-heatmap-row{
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.trial-expired-heatmap-dow{
  display: grid;
  grid-template-rows: repeat(7, 14px);
  gap: 2px;
  font-size: var(--type-label-size);
  color: var(--text-meta);
  line-height: 14px;
  text-align: center;
}

.trial-expired-heatmap{
  display: grid;
  grid-template-columns: repeat(5, 14px);
  grid-template-rows: repeat(7, 14px);
  gap: 2px;
}

.trial-expired-heatmap-cell{
  width: 14px;
  height: 14px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.04);
}

/* Intensity 0 (empty) — barely visible scaffolding. */
.trial-expired-heatmap-cell--i0{
  background: var(--surface-2);
  opacity: 0.5;
}

/* Intensity 1–3 (active) — signature blue at proportional opacity. */
.trial-expired-heatmap-cell--i1{ background: rgba(33, 150, 243, 0.40); }
.trial-expired-heatmap-cell--i2{ background: rgba(33, 150, 243, 0.60); }
.trial-expired-heatmap-cell--i3{ background: rgba(33, 150, 243, 0.80); }

.trial-expired-heatmap-annotation{
  font-size: var(--type-meta-size);
  color: var(--text-meta);
  margin-left: auto;
}

/* Theme proportion bar — single horizontal stacked bar; segment widths
   set inline via style attribute. Each segment fills with its theme's
   hash-derived rotation colour at 50% alpha. White separators come from
   a 0.5px box-shadow inset on the right of each segment. */
.trial-expired-proportion-bar{
  display: flex;
  width: 100%;
  height: 20px;
  border-radius: 4px;
  overflow: hidden;
  background: var(--surface-2);
}

.trial-expired-proportion-seg{
  display: block;
  height: 100%;
  box-shadow: inset -0.5px 0 0 rgba(255, 255, 255, 0.5);
}

.trial-expired-proportion-seg:last-child{
  box-shadow: none;
}

.trial-expired-proportion-seg--blue{ background: rgba(33, 150, 243, 0.50); }
.trial-expired-proportion-seg--purple{ background: rgba(167, 139, 250, 0.50); }
.trial-expired-proportion-seg--teal{ background: rgba(45, 212, 191, 0.50); }
.trial-expired-proportion-seg--coral{ background: rgba(251, 113, 133, 0.50); }
.trial-expired-proportion-seg--amber{ background: rgba(251, 191, 36, 0.50); }
.trial-expired-proportion-seg--green{ background: rgba(74, 222, 128, 0.50); }
.trial-expired-proportion-seg--other{
  background: var(--text-meta);
  opacity: 0.30;
}

.trial-expired-proportion-list{
  font-size: var(--type-meta-size);
  color: var(--text-meta);
  margin: 0;
  line-height: 1.4;
}

.trial-expired-stats-grid{
  /* Cross-surface spacing parity: mirrors the dashboard behavioural
     metric grid's 6px row-gap + 6px column-gap + stretch-to-tallest tile
     pair. Same vocabulary across both surfaces means a glance reads as
     consistent rhythm. */
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  row-gap: 6px;
  column-gap: 6px;
  align-items: stretch;
  grid-auto-rows: 1fr;
}

.trial-expired-stat-tile{
  /* Inherits .admin-metric-tile base: --surface background, hover-lightens,
     padding/gap, canonical 11px-meta label + 16px-bright-500 value.
     Apply the canonical asymmetric accent edge: 3px tier-accent stripe on
     the left + ::after pseudo carrying 0.5px hairlines on the other three
     sides + 0 8px 8px 0 radius. Native top/right/bottom borders suppressed
     so the 3px border-left has nothing to mitre against at the square left
     corners. --tier-accent cascades from the body tier class; falls back to
     signature-blue callout-accent if a non-tier-classed viewer reaches
     this page. Stripe is identity (the viewer's tier), not state —
     unchanged on hover. */
  position: relative;
  border-top: none;
  border-right: none;
  border-bottom: none;
  border-left: 3px solid var(--tier-accent, var(--callout-accent));
  border-radius: 0 8px 8px 0;
}
.trial-expired-stat-tile::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 8px 8px 0;
  pointer-events: none;
}

/* Stat tile interior — flex row anchoring the leading Tabler icon next to
   the label text, then the value below. Mirrors the dashboard behaviour
   grid pattern (icon inline with label, leading-positioned). */
.trial-expired-stat-tile .admin-metric-label{
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.trial-expired-stat-tile .admin-metric-label .ti{
  font-size: 14px;
  line-height: 1;
  color: var(--text-meta);
}
.trial-expired-stat-tile .admin-metric-value{
  /* Allow long handles (e.g. @business.shorts) to wrap to a second line
     inside the tile rather than overflowing the rounded right edge. Grid
     row stretches all tiles to the tallest tile via align-items: stretch
     on the parent, so the row stays visually uniform. */
  word-break: break-word;
  overflow-wrap: anywhere;
  line-height: 1.2;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.trial-expired-cta-stack{
  margin: 0;
}

.trial-expired-cta-form{
  margin: 0;
}

.trial-expired-cta-row{
  /* Stripe-bearing CTA row. Canonical asymmetric accent edge: native top /
     right / bottom borders suppressed (so the 3px border-left has nothing
     to mitre against at square left corners) + ::after pseudo carrying the
     three 0.5px hairlines + 0 10px 10px 0 radius. Background is plain
     --surface — stripe carries the tier colour signal alone. Label text
     itself is meta-dim by default; the bright .trial-expired-cta-label
     span (action verb) is the only bright element, while the price span
     and the trailing arrow stay meta-dim. Hover lightens background; the
     arrow brightens slightly as a click affordance. */
  position: relative;
  width: 100%;
  display: flex;
  align-items: center;
  gap: 0;
  padding: 12px 14px;
  background: var(--surface);
  border-top: none;
  border-right: none;
  border-bottom: none;
  border-left: 3px solid var(--tier-accent, var(--callout-accent));
  border-radius: 0 10px 10px 0;
  color: var(--text-meta);
  font-size: var(--type-body-size);
  font-weight: 400;
  text-align: left;
  cursor: pointer;
  transition: background 140ms ease;
}
.trial-expired-cta-row::after{
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border-top: 0.5px solid var(--border);
  border-right: 0.5px solid var(--border);
  border-bottom: 0.5px solid var(--border);
  border-radius: 0 10px 10px 0;
  pointer-events: none;
}

.trial-expired-cta-row:hover,
.trial-expired-cta-row:focus-visible{
  background: var(--surface-2);
}

.trial-expired-cta-label{
  color: var(--text);
  font-weight: 500;
}

.trial-expired-cta-price{
  color: var(--text-meta);
  flex: 1;
}

.trial-expired-cta-arrow{
  margin-left: auto;
  font-size: 16px;
  line-height: 1;
  color: var(--text-meta);
  transition: color 140ms ease;
}

.trial-expired-cta-row:hover .trial-expired-cta-arrow,
.trial-expired-cta-row:focus-visible .trial-expired-cta-arrow{
  color: var(--text);
}

.trial-expired-footer-link{
  display: block;
  text-align: center;
  font-size: var(--type-meta-size);
  color: var(--text-meta);
  text-decoration: none;
  padding: 4px 8px;
  transition: color 140ms ease;
}

.trial-expired-footer-link:hover,
.trial-expired-footer-link:focus-visible{
  color: var(--text);
}

@media (max-width: 560px){
  /* Stats grid drops to 2×2 on narrow viewports; mosaic flex-wraps
     automatically (no explicit breakpoint needed). X-axis-only mobile
     rule: do NOT alter row-gap / column-gap / padding / margin Y-axis
     values here — vertical rhythm stays global. */
  .trial-expired-stats-grid{
    grid-template-columns: 1fr 1fr;
  }
}

/* Tier card — trial-expired amber + interest-map CTA on /ui/upgrade.
   The Curator row in this state is rendered as an <a> (not <form><button>),
   linking to /ui/trial-expired. The price-line slot carries:
     [amber "Trial expired"] · [meta-dim "View your interest map →"]
   on hover the CTA portion brightens. Amber matches the action-needed
   semantic established by the new-item connection traffic light. */
.account-plan-row-trial-expired-link{
  /* Anchor variant of .account-plan-row. The base .account-plan-row rule
     already covers the layout; here we just strip anchor defaults. */
  text-decoration: none;
}

.account-plan-row-trial-expired-link:hover,
.account-plan-row-trial-expired-link:focus-visible{
  text-decoration: none;
}

.account-plan-row-trial-expired .trial-expired-amber{
  color: var(--pill-processing-fg);
}

.account-plan-row-trial-expired .trial-expired-sep{
  color: var(--text-meta);
  margin: 0 4px;
}

.account-plan-row-trial-expired .trial-expired-cta{
  color: var(--text-meta);
  display: inline-flex;
  align-items: center;
  gap: 4px;
  transition: color 140ms ease;
}

.account-plan-row-trial-expired-link:hover .trial-expired-cta,
.account-plan-row-trial-expired-link:focus-visible .trial-expired-cta{
  color: var(--text);
}

/* ---------------------------------------------------------------------------
   Search (full-text search v1, 2026-06-09). The functional input is the global
   top-strip box (one box, every surface — `.top-strip-search` is now a real
   <form>/<input>); /ui/search renders the live results region only. Rows reuse
   the .recent-save-* vocabulary; search-specific bits are the help panel, the
   <mark> snippet, the multi-field badges, and the (inert) transcript seam.
   --------------------------------------------------------------------------- */

/* Top-strip search as a real input (the `.top-strip-search` box rules — flex,
   pill, fill — are unchanged; these style the inner field). */
.top-strip-search{ margin: 0; overflow: visible; }
.top-strip-search-icon-wrap{ display: inline-flex; align-items: center; flex-shrink: 0; }
.top-strip-search-input{
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  /* The global `input,textarea,select` rule gives every field a ~10px radius.
     On this field the caret sits ~2px from the input's own left edge — i.e.
     UNDER that rounded top-left corner — so its top got clipped. Zero the
     input's own radius; the .top-strip-search pill supplies the visible shape. */
  border-radius: 0;
  background: transparent;
  color: var(--text);
  /* 0.85rem was an off-scale literal and read oversized against the wordmark on
     desktop. --type-preview-size (0.75rem) is the scale step for this weight of
     secondary text, so the field now sits on the type scale rather than beside it. */
  font-size: var(--type-preview-size);
  padding: 0 0 0 2px;
  line-height: 1.4;
}
.top-strip-search-input::placeholder{ color: var(--text-meta); }
.top-strip-search-input:focus-visible{ outline: none; }
/* Active state: the box outline goes full white (identity chrome — NOT the
   viewer's tier accent). */
.top-strip-search:focus-within{ border-color: var(--text); color: var(--text); }

/* X clear button — RHS of the input, shown only when there's text. Clears the
   field, refocuses, and dispatches an input event so the live search resets to
   the help panel. */
.top-strip-search-clear{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 18px;
  height: 18px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--text-meta);
  cursor: pointer;
}
.top-strip-search-clear:hover{ color: var(--text); }
.top-strip-search-clear svg{ width: 12px; height: 12px; display: block; }
/* The clear button is toggled via the `hidden` attribute (JS: clear.hidden =
   !input.value). Without this guard the author `display: inline-flex` above beats
   the UA `[hidden]{display:none}` rule, so the X shows even on an empty box (and
   clicking it does nothing). Mirrors the .modal-overlay[hidden] / .more-menu[hidden]
   guards (canon). */
.top-strip-search-clear[hidden]{ display: none; }

/* The count line leads the result list — give it the same breathing room a card
   heading gets above its content (canon: ~12px head-to-list gap). */
.search-result-count{ font-size: 0.8rem; }

/* ── Bulk multi-select -> add-to-channel (Phase 2: Search) ─────────────────── */
/* Header row: result count on the left, the "Select" toggle (+ "Select all N"
   once in select mode) on the right. */
.search-result-head{
  display: flex; align-items: center; gap: 10px; margin-bottom: 12px;
}
.search-result-head .search-result-count{ margin: 0; }
.bulk-toggle, .bulk-select-all{
  margin-left: auto; flex-shrink: 0;
  background: none; border: none; cursor: pointer;
  font: inherit; font-size: 0.8rem; color: var(--text-meta);
  padding: 4px 8px; border-radius: 8px;
}
.bulk-select-all{ margin-left: 0; color: var(--tier-accent, #0698ea); }
.bulk-toggle:hover, .bulk-select-all:hover{ color: var(--text); background: var(--btn-hover); }
.bulk-toggle.is-active{ color: var(--tier-accent, #0698ea); }

/* The per-row select box — reserved (no layout shift) but collapsed until the
   scope enters select mode. */
.row-select{
  flex: 0 0 0; width: 0; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  transition: width 120ms ease, margin 120ms ease;
}
.search-results-scope.is-selecting .row-select{ flex-basis: 26px; width: 26px; margin-right: 2px; }
.row-select-box{
  width: 18px; height: 18px; border-radius: 5px;
  border: 1.5px solid var(--border-2, var(--border)); background: var(--surface-2);
}
.search-results-scope.is-selecting .recent-save-row{ cursor: pointer; }
.search-results-scope.is-selecting .recent-save-row.is-selected{
  background: color-mix(in srgb, var(--tier-accent, #0698ea) 12%, var(--surface));
}
.recent-save-row.is-selected .row-select-box{
  background: var(--tier-accent, #0698ea); border-color: var(--tier-accent, #0698ea);
  position: relative;
}
.recent-save-row.is-selected .row-select-box::after{
  content: ''; position: absolute; left: 5px; top: 1px; width: 5px; height: 10px;
  border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg);
}
/* In select mode the row's own links are inert (JS intercepts the click); keep
   them from looking clickable-to-navigate. */
.search-results-scope.is-selecting .recent-save-content-link,
.search-results-scope.is-selecting .search-hit-fields a{ pointer-events: none; }

/* Sticky bottom action bar. */
.bulk-action-bar{
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));
  background: var(--surface-chrome); border-top: 0.5px solid var(--border);
  box-shadow: var(--shadow-md);
}
.bulk-action-bar[hidden]{ display: none; }
.bulk-action-bar-inner{
  max-width: var(--content-max-width, 980px); margin: 0 auto;
  display: flex; align-items: center; gap: 12px;
}
.bulk-action-count{ font-size: 0.9rem; color: var(--text-preview); }
.bulk-action-count strong{ color: var(--text); }
.bulk-action-buttons{ margin-left: auto; display: flex; align-items: center; gap: 8px; }
.bulk-action-add{
  background: var(--tier-accent, #0698ea); color: #fff; border: none; cursor: pointer;
  font: inherit; font-weight: 600; padding: 9px 16px; border-radius: 10px;
}
.bulk-action-add:hover{ filter: brightness(1.06); }

/* When the inline live-results region is showing on a content page (help panel,
   results, or empty state), separate it from the page content block below. On
   /ui/search the region is the whole content, so the trailing gap is harmless. */
#search-live-region:not([hidden]){ margin-bottom: 18px; }

/* Empty state — a light, forward-looking line (it might match once you save it)
   over a muted nudge toward the four searchable dimensions. */
/* Tighten the two empty-state lines ~20% closer than the default .stack 12px. */
.search-empty{ gap: 9.6px; }
.search-empty-line{ margin: 0; }
.search-empty-hint{ margin: 0; font-size: 0.82rem; }

/* No-query help panel (replaces the removed on-page input) — the four searchable
   dimensions, each with a glyph, + a hint.
   The column count adapts to the PANEL's own width, not the viewport: the panel
   sits inside the content column (rail + padding + max-width), so panel width !=
   viewport width and a viewport media query switches at the wrong moment. Make
   the panel a container and query its inline-size (rules below). */
/* The legend is no longer a single card — it's a bare helper container (like
   .how-to-save) whose own flex gap evenly spaces the lead line, the tile grid,
   and the hint (the old .card .stack 12px gap is reproduced here). */
.search-help{ container-type: inline-size; display: flex; flex-direction: column; gap: 12px; }
.search-help > p{ margin: 0; }
.search-help-dims{
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  /* Wide: four peer tiles across; the container ladder below drops to 2 then 1. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  /* Equal-height rows: with 5 tiles a lone tile (the 5th, "mentions") sits alone in
     the last row and would size to its own content — taller/shorter than its peers,
     visible on mobile's 2-col layout. 1fr rows make every row (and so every tile,
     via the default stretch) the same height. */
  grid-auto-rows: 1fr;
  gap: 8px;
}
/* Each dimension is a PEER TILE in the onboarding helper-card vocabulary
   (mirrors .how-to-save-step: surface fill, hairline, centred glyph-over-label,
   paint-only hover lift) — but with NO number badge, because the legend is a
   parallel KEY, not sequential steps. */
.search-help-dim{
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 7px;
  padding: 14px 12px;
  min-width: 0;
  background: var(--surface-2);
  border: 0.5px solid var(--border);
  border-radius: 10px;
}
/* No hover/active state — these tiles are a static REPRESENTATION of the search
   dimensions, not interactive targets (they're <li>, not links), so a lift would
   wrongly imply they do something. */
/* Bare coloured glyph on the tile (no backplate — the tile IS the surface now).
   Fixed height reserves a consistent glyph row so the labels below line up across
   the four tiles regardless of each asset's own height. The glyph keeps its
   palette colour on hover (it's the key); only the tile lifts. */
.search-help-glyph{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  flex-shrink: 0;
}
/* Each glyph takes a DISTINCT colour from the wider tag/trending palette (not the
   viewer's tier accent) — four picks, no duplication across the four rows. */
/* Glyph hues are spread across the wheel (gold/blue/coral/green) so no two
   are adjacent AND none collides with a tier accent. Transcript was --tag-purple
   (identical to the Curator accent #A78BFA); Caption was --tag-teal (a green
   neighbour of the Tag green). See 2026-06-11 search-glyph-palette pass. */
.search-help-dims li:nth-child(1) .search-help-glyph{ color: var(--tag-gold); }
.search-help-dims li:nth-child(2) .search-help-glyph{ color: var(--tag-blue); }
.search-help-dims li:nth-child(3) .search-help-glyph{ color: var(--tag-coral); }
.search-help-dims li:nth-child(4) .search-help-glyph{ color: var(--tag-green); }
.search-help-dims li:nth-child(5) .search-help-glyph{ color: var(--tag-teal); }
.search-help-glyph .ti{ font-size: 16px; }
/* Custom brand glyphs (Spoken / filled Channel triangle / Tag) — currentColor so
   the per-glyph palette colour applies; sized by height, width auto to keep each
   asset's own aspect ratio. */
/* Bare on the tile (no backplate). Sized for EQUAL OPTICAL WEIGHT, not equal
   bounding box: the SOLID glyphs (Spoken quotes, Caption bars) read heavy so they
   sit smallest; the OUTLINE glyphs (Channel triangle med-stroke, Tag thin-stroke)
   read light so they sit larger to carry the same mass. Overall ~20% down from the
   first bare pass. */
.search-help-svg{ height: 14px; width: auto; display: block; }
.search-help-svg--spoken{ height: 13px; }    /* solid quotes — heaviest */
.search-help-svg--caption{ height: 13px; }    /* solid bars — heavy */
.search-help-svg--triangle{ height: 15px; }   /* outline triangle — medium */
.search-help-svg--tag{ height: 16px; }         /* thin outline — lightest, largest */
.search-help-svg--mentions{ height: 15px; }    /* solid user mark — medium */
/* Centred label stack matching the onboarding helper-card label (.how-to-save-label):
   preview-weight name + meta sub. */
.search-help-text{ display: flex; flex-direction: column; align-items: center; gap: 1px; line-height: 1.2; }
.search-help-text strong{ font-size: 0.8rem; font-weight: 500; color: var(--text-preview); }
.search-help-text .muted{ font-size: 0.7rem; }
/* Lead + hint are instructional BODY copy (like empty-state hints / wizard step
   descriptions), so they read preview brightness, not muted — overriding the
   `muted` class on the markup. (Per the button-instruction-vs-body-copy canon.) */
.search-help-lead{ color: var(--text-preview); }
/* The "4 ways:" count is tier-coloured to pick out the number of search dimensions
   (the four legend tiles below). */
.search-help-count{ color: var(--tier-accent, #0698ea); font-weight: 600; }
.search-help-hint{ margin: 0; font-size: 0.82rem; color: var(--text-preview); }
/* No-data state: the "how to save?" link out to the homepage guide. Tier-coloured,
   brightens to text on hover (paint-only), no underline — matching the onboarding
   guide's CTA link vocabulary. */
.search-help-link{ color: var(--tier-accent, #0698ea); text-decoration: none; transition: color 140ms ease; }
.search-help-link:hover{ color: var(--text); }

/* ── Browser-extension pairing page (/ui/account/extension) ───────────────── */
/* Standard section rhythm: --space-5 (12px) between sections; --space-3 (8px)
   inside a grouped block (heading→intro, eyebrow→card→warn, label→rows). */
.ext-page{ display: flex; flex-direction: column; gap: var(--space-5); }
.ext-head{ display: flex; flex-direction: column; gap: var(--space-3); }
.ext-page .card-heading{ display: flex; align-items: center; min-height: var(--icon-btn-size); margin: 0; }
.ext-intro{ margin: 0; color: var(--text-preview); font-size: 0.85rem; line-height: 1.45; }

/* Connect-code reveal: an EDGE-ACCENT square container (same shape as the IG
   connect card — left stripe + hairline, 0 10px 10px 0), green stripe + soft tint
   for a positive one-time reveal, with the canonical copy icon at the right. */
.ext-code-block{ display: flex; flex-direction: column; gap: var(--space-3); }
/* Fixed 52px card (matches the IG connect cards). The label + code stack INSIDE the
   body like the IG card's title + sub; the copy icon sits in the action slot. */
.ext-code-card{
  display: flex;
  align-items: center;
  gap: var(--space-3);
  height: 52px;
  padding: 0 10px 0 14px;
  --stripe-rest: var(--pill-done-fg);
  background: color-mix(in srgb, var(--pill-done-fg) 8%, transparent);
}
/* Leading glyph, mirroring the IG connected card's green glyph. */
.ext-code-icon{ flex-shrink: 0; display: inline-flex; align-items: center; color: var(--pill-done-fg); }
.ext-code-icon svg{ width: 18px; height: 18px; }
.ext-code-body{ flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: center; gap: 1px; }
/* Sentence case (NOT a section-label) to match the IG card's title "Connected". */
.ext-code-eyebrow{ font-weight: 500; color: var(--pill-done-fg); }
.ext-code{
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.74rem;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  user-select: all;
}
/* The copy icon reuses .icon-btn (square container); flips green on success. */
.ext-copy{ flex-shrink: 0; }
.ext-copy.is-copied{ color: var(--pill-done-fg); }
.ext-warn{ margin: 0; color: var(--text-preview); font-size: 0.78rem; line-height: 1.4; }

/* Generate = full-width, MUTED at rest → BRAND-BLUE on hover (the app's
   proceed-button dynamic). Paint-only per the hover canon. */
/* Install (download .xpi) — primary first step, so brand-blue at rest.
   Extra vertical breathing room around the button (gap to its note + margin to
   the surrounding intro / connections) so it reads as a calm, distinct step. */
/* Extension install — reuses the /get-the-app placard cards (.get-app-steps /
   .get-app-platform): tier-coloured glyph + heading on BOTH cards. The cards are
   flex columns so their buttons bottom-align across the row. */
.ext-install-grid{ margin: var(--space-3) 0; }
.ext-install-grid .get-app-platform{ display: flex; flex-direction: column; }
.ext-install-grid .get-app-platform ol,
.ext-install-grid .ext-soon-note{ margin-bottom: var(--space-5); }
.ext-install-grid .ext-install-btn,
.ext-install-grid .ext-soon-btn{ margin-top: auto; }
.ext-soon-note{ margin: 0; color: var(--text-meta); font-size: 0.85rem; line-height: 1.6; }
/* Shared button shape (download + the inert Chrome placeholder). */
.ext-install-btn,
.ext-soon-btn{
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  min-height: 44px;
  border-radius: 10px;
  font-size: 0.9rem;
  font-weight: 600;
  text-decoration: none;
  box-sizing: border-box;
}
/* Download: muted + hairline at rest, fills brand blue on hover. */
.ext-install-btn{
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
  transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}
.ext-install-btn:hover,
.ext-install-btn:focus-visible{
  background: var(--accent-blue); border-color: var(--accent-blue); color: #fff; outline: none;
}
/* Chrome placeholder: same shape, inert — stays muted, never turns blue. */
.ext-soon-btn{
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text-meta);
  cursor: default;
}
.ext-install-btn .icon,
.ext-soon-btn .icon{ width: 18px; height: 18px; }

.ext-generate{
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 44px;
  border: 0;
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text-meta);
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 140ms ease, color 140ms ease;
}
.ext-generate:hover,
.ext-generate:focus-visible{ background: var(--accent-blue); color: #fff; outline: none; }

/* Active-connection rows — settings-row vocabulary (52px, label + meta + action). */
.ext-connections{ display: flex; flex-direction: column; gap: var(--space-3); }
/* Zero the empty-state <p>'s UA margin so the title→text gap is the flex 8px
   (matching heading→intro), not 8px + the paragraph margin. */
.ext-connections .empty-state-hint{ margin: 0; }
/* Rows borrow the IG-card edge-accent vocabulary (left stripe + hairline,
   0 10px 10px 0) — apply .edge-accent in the markup; the global --stripe-rest
   (var(--border)) gives the neutral muted stripe. */
.ext-conn{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 52px;
  padding: 8px 14px;
  box-sizing: border-box;
  background: var(--surface-2);
}
.ext-conn-name{ display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ext-conn-name strong{ font-size: 0.85rem; font-weight: 600; color: var(--text); }
.ext-conn-meta{ font-size: 0.76rem; color: var(--text-meta); }
/* Revoke is destructive → muted at rest, danger-red on hover (paint-only). */
.ext-revoke{
  flex-shrink: 0;
  padding: 4px 6px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--text-meta);
  font-size: 0.8rem;
  cursor: pointer;
  transition: color 140ms ease;
}
.ext-revoke:hover{ color: var(--pill-failed-fg); }
/* Hovering Revoke turns the WHOLE row's accent stripe + a soft tint red (mirrors
   the IG card's disconnect-hover), so the destructive action reads on the container,
   not just the button. Transitions ride the .edge-accent base transition. */
.ext-conn:has(.ext-revoke:hover){
  border-left-color: var(--pill-failed-fg);
  background: rgba(248, 113, 113, 0.10);
}

/* Seat the title in the SAME heading-row rhythm as every other page: a 40px
   (--icon-btn-size) centred row + an 8px gap to what follows, matching
   .dashboard-section-head. This puts "Search your stuff" at the same vertical
   position as the Library / Favourite Creators / Dashboard titles below the strip,
   instead of riding higher as a tight block. Applies to BOTH the active-user help
   panel and the new-user (--empty) state. */
.search-help-title{
  display: flex;
  align-items: center;
  min-height: var(--icon-btn-size);
  margin: 0 0 8px;
}
#search-live-region .how-to-save{ margin-top: 24px; }

/* Recent searches (search page only) — a bare chip row between the title and the
   help panel; each chip re-runs its query and carries its own remove ×. No
   "Recent" label and no clear-all (client-side history stays small). Populated
   from localStorage. */
.search-recents{ margin: 0 0 14px; }
.search-recents-chips{ display: flex; flex-wrap: wrap; gap: 8px; }
/* Cat-pill treatment: a persistent hairline (not a hover-appearing border) +
   12px radius (not a full lozenge), muted so it doesn't overload. */
.search-recent-chip{
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-height: 22px;
  padding: 1px 5px 1px 10px;
  border-radius: 12px;
  background: var(--surface-2);
  border: 0.5px solid var(--border);
}
.search-recent-chip-go{
  appearance: none;
  -webkit-appearance: none;
  display: inline-flex;
  align-items: center;
  border: 0;
  background: transparent;
  padding: 0;
  /* line-height:1 + the flex centring above keep the query text vertically
     centred against the × within the pill. */
  line-height: 1;
  font-size: 0.8rem;
  color: var(--text-preview);
  cursor: pointer;
  transition: color 150ms ease;
}
.search-recent-chip-go:hover{ color: var(--text); }
.search-recent-chip-x{
  appearance: none;
  -webkit-appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--text-meta);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  transition: color 150ms ease, background 150ms ease;
}
.search-recent-chip-x:hover{ color: var(--text); }
/* Container-query ladder — the "min width per item, else next layout" rule. Each
   dimension needs ~155px to hold its glyph + the widest caption ("the post's own
   words") without breaking; below 4×that (+gaps) the panel drops to a 2×2 grid,
   and below 2×that it stacks to a single column. Querying the PANEL width means
   the switch fires when the cells actually run out of room, at any viewport.
   The 560px viewport rule is a fallback for engines without @container support. */
/* Responsive in the same way as the onboarding helper cards: tiles reflow 4 -> 2,
   then at the narrowest the single column switches to a left-aligned ROW layout
   (glyph beside label) so a full-width centred tile never floats awkwardly — the
   same stack treatment as .how-to-save-step at its container breakpoint. */
@container (max-width: 600px){ .search-help-dims{ grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@container (max-width: 300px){
  .search-help-dims{ grid-template-columns: 1fr; }
  /* More horizontal inset in the stacked row layout so the glyph isn't tight to
     the card's left edge (the centred states don't need it). */
  .search-help-dim{ flex-direction: row; justify-content: flex-start; text-align: left; gap: 12px; padding: 13px 16px; }
  .search-help-text{ align-items: flex-start; }
}

/* Snippet — same body-copy treatment as the Recents row's .recent-save-desc
   (meta size, --text-preview, 1.12 line-height) so the two listings read
   identically. Clamped to ONE line: title + 1 snippet line + the match-field
   badge row equals the Recents row's title + 2-line desc, so the row heights
   match. The matched run is lifted by <mark> (paint-only tier-accent wash). */
.search-hit-snippet{
  color: var(--text-preview);
  font-size: var(--type-meta-size);
  line-height: 1.12;
  margin-top: 2px;
  /* A TRUE single line (nowrap + ellipsis), not a -webkit-line-clamp box — the
     clamp box left a 1px sliver of the next line's ascenders peeking above the
     cut. The snippet text is already windowed around the match server-side. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The snippet mark takes the matched field's glyph colour (gold/blue/coral/green)
   via --snippet-mark, set by the .search-hit-snippet--{field} modifier. Falls back
   to the brand/tier tint if no field class is present. */
.search-hit-snippet{ --snippet-mark: var(--tier-accent, #0698ea); }
.search-hit-snippet--transcript{ --snippet-mark: var(--tag-gold); }
.search-hit-snippet--caption{ --snippet-mark: var(--tag-blue); }
.search-hit-snippet--channel{ --snippet-mark: var(--tag-coral); }
.search-hit-snippet--tag{ --snippet-mark: var(--tag-green); }
.search-hit-snippet--mentions{ --snippet-mark: var(--tag-teal); }
.search-hit-snippet mark{
  background: color-mix(in srgb, var(--snippet-mark) 26%, transparent);
  color: var(--text);
  border-radius: 3px;
  padding: 0 1px;
}
/* Jump-to-moment highlight — the search term lit up on the detail page after a
   click-through (?q=). Coloured by LOCATION (set via data-section in the JS) so the
   glyph language extends onto the detail page: Caption reads blue (the post's own
   words); Transcription + the synthesis (Summary / Key Points), all spoken-content-
   derived, read gold (the default). */
.search-jump-mark{
  --jump-c: var(--tag-gold);
  background: color-mix(in srgb, var(--jump-c) 26%, transparent);
  color: var(--text);
  border-radius: 3px;
  padding: 0 1px;
}
.search-jump-mark[data-section="caption"]{ --jump-c: var(--tag-blue); }

/* Match-field badges — siblings of the title link (so each carries its own
   destination). Field badges (tag/channel/caption) link to the plain detail
   page; the transcript badge is the jump-to-moment seam. */
.search-hit-fields{
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 4px;
}
/* Match-field indicator — NOT a filled pill. A small rounded-square glyph tile
   (echoing the legend tiles) + a sentence-case label in the field colour beside
   it (the cat-pill vocabulary: coloured text, no uppercase). The field colour is
   carried in --field-c so the tile bg, glyph, and label all derive from one var. */
.search-hit-field{
  display: inline-flex;
  align-items: center;
  gap: 6px;
  text-decoration: none;
  color: var(--field-c);
}
.search-hit-field-icon{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  border-radius: 6px;
  /* Neutral tile to MATCH the legend's .search-help-glyph (var(--surface-2)) —
     the colour identity rides on the glyph alone, not the tile. */
  background: var(--surface-2);
  color: var(--field-c);
}
.search-hit-field-label{
  font-size: var(--type-meta-size);
  line-height: 1.1;
  /* Muted: the COLOURED GLYPH carries the dimension signal; the "in transcript"
     label + the ▸ moment are quiet meta chrome so the emphasis stays on the title
     and snippet (the content). The moment span inherits this colour. */
  font-weight: 400;
  color: var(--text-meta);
}
/* Link variant (caption / channel / tag) gives a faint field-colour tint on hover
   for feedback (rest stays neutral like the legend). */
a.search-hit-field:hover .search-hit-field-icon{ background: color-mix(in srgb, var(--field-c) 16%, transparent); }
/* Per-field colour — matches the help-panel glyph palette so the indicator reads
   as the same dimension: Transcript=purple, Caption=teal, Channel=coral, Tag=green. */
/* Match the legend hues above (gold/blue/coral/green). */
.search-hit-field--transcript{ --field-c: var(--tag-gold); }
.search-hit-field--caption{ --field-c: var(--tag-blue); }
.search-hit-field--channel{ --field-c: var(--tag-coral); }
.search-hit-field--tag{ --field-c: var(--tag-green); }
.search-hit-field--mentions{ --field-c: var(--tag-teal); }
/* The glyph inside the tile (same SVG as the legend, currentColor → field colour).
   Per-field heights mirror the legend's optical balance, scaled to tile size. */
.search-hit-glyph{ width: auto; flex-shrink: 0; display: block; }
.search-hit-field--transcript .search-hit-glyph{ height: 9px; }
.search-hit-field--caption .search-hit-glyph{ height: 9.5px; }
.search-hit-field--channel .search-hit-glyph{ height: 10.5px; }
.search-hit-field--tag .search-hit-glyph{ height: 11.5px; }
.search-hit-field--mentions .search-hit-glyph{ height: 11px; }
/* Transcript seam — present for any transcript hit; INERT in v1 (no href). The
   fast-follow turns it into a link to the detail page. The ▸m:ss timecode shows
   when the phrase was spoken in a timecoded segment. (Colour comes from the
   --transcript modifier above.) */
.search-hit-moment{ font-variant-numeric: tabular-nums; }

/* ============================================================
   Get-the-app PWA install funnel (/get-the-app) + the desktop QR nudge.
   ============================================================ */
.get-app-page{ display: flex; flex-direction: column; gap: var(--space-6); }
.get-app-head{ display: flex; flex-direction: column; gap: var(--space-3); }
/* Heading seated in the same 40px min-height row as every other page heading
   (.search-help-title / .dashboard-section-head) so the space above "Get the app"
   matches the rest of the app. */
.get-app-head .card-heading{ display: flex; align-items: center; min-height: var(--icon-btn-size); }
.get-app-lead{ margin: 0; color: var(--text-preview); font-size: 0.9rem; line-height: 1.5; max-width: 62ch; }
/* QR block — bare (no container), left-justified: the QR on a small white tile
   (navy-on-transparent needs a light backing to scan) on the LEFT, copy on the
   RIGHT. */
.get-app-qr-card{ display: flex; align-items: center; justify-content: flex-start;
  gap: var(--space-6); width: 100%; }
.get-app-qr{ width: 124px; height: 124px; flex-shrink: 0; display: block;
  background: #fff; border-radius: 8px; padding: 8px; box-sizing: border-box; }
.get-app-qr-copy{ display: flex; flex-direction: column; gap: 6px; max-width: 42ch; }
.get-app-qr-copy strong{ font-size: 1rem; color: var(--text); }
.get-app-qr-copy span{ font-size: 0.85rem; color: var(--text-preview); }
/* Responsive: two platform cards side by side, collapsing to one when narrow.
   Styled like the home instructional placards (.how-to-save-step): --surface-2
   fill, hairline, 10px radius, paint-only hover lift, tier-coloured heading. */
.get-app-steps{ display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--space-5); }
.get-app-platform{ background: var(--surface-2); border: 0.5px solid var(--border);
  border-radius: 10px; padding: var(--space-6);
  transition: background 140ms ease, border-color 140ms ease; }
.get-app-platform:hover{ background: var(--surface); border-color: var(--border-2); }
.get-app-platform-head{ display: flex; align-items: center; gap: 8px; font-weight: 600;
  color: var(--tier-accent, var(--accent-blue)); margin-bottom: var(--space-4); font-size: 0.9rem; }
.get-app-platform-glyph{ width: 18px; height: 18px; flex-shrink: 0; }
.get-app-platform ol{ margin: 0; padding-left: 1.2em; color: var(--text-body); font-size: 0.85rem; line-height: 1.85; }
.get-app-platform ol strong{ color: var(--text); }
/* Tap-to-copy install URL — the URL keeps its bright bold look (like a <strong>);
   the copy glyph sits at preview brightness (matching the step body text) and
   brightens on hover. No background (overrides the global button:hover lozenge).
   On copy the text + a "Copied" label go success-green, glyph hides ~1.6s. */
.get-app-url{ display: inline-flex; align-items: center; gap: 4px; font: inherit;
  font-weight: 600; color: var(--text); background: none; border: 0; padding: 0;
  cursor: pointer; vertical-align: baseline; }
.get-app-url:hover{ background: none; }
.get-app-url-icon{ width: 13px; height: 13px; flex-shrink: 0; color: var(--text-preview);
  transition: color 140ms ease; }
.get-app-url:hover .get-app-url-icon{ color: var(--text); }
.get-app-url-done{ display: none; color: var(--pill-done-fg); font-weight: 500; }
.get-app-url.is-copied .get-app-url-text{ color: var(--pill-done-fg); }
.get-app-url.is-copied .get-app-url-icon{ display: none; }
.get-app-url.is-copied .get-app-url-done{ display: inline; }
@media (max-width: 560px){ .get-app-qr-card{ display: none; } }   /* on a phone you're already here */
/* Note card (Firefox → extension / Safari → app pointers): same placard chrome as
   the step cards, but short prose + a text link instead of numbered steps. */
.get-app-platform--note p{ margin: 0 0 var(--space-4); color: var(--text-body);
  font-size: 0.85rem; line-height: 1.6; }
.get-app-note-link{ font-weight: 600; color: var(--tier-accent, var(--accent-blue));
  text-decoration: none; }
.get-app-note-link:hover{ text-decoration: underline; }
.ext-install-grid .get-app-platform--note p{ margin-bottom: var(--space-5); }
.ext-install-grid .get-app-note-link{ margin-top: auto; }
/* Browser-aware reveal — _platform_detect stamps html[data-rn-plat]; hide the cards
   that DON'T match the visitor's browser so they see their own path. A matching card
   keeps its natural display (block here / flex in .ext-install-grid — never restored,
   so the flex layout is preserved). Only KNOWN platforms hide anything → an unknown
   UA or no-JS leaves data-rn-plat unset (or "other") and every card stays visible
   (the honest fallback). */
html[data-rn-plat="ios"]      .get-app-platform[data-plat]:not([data-plat~="ios"]),
html[data-rn-plat="android"]  .get-app-platform[data-plat]:not([data-plat~="android"]),
html[data-rn-plat="chromium"] .get-app-platform[data-plat]:not([data-plat~="chromium"]),
html[data-rn-plat="safari"]   .get-app-platform[data-plat]:not([data-plat~="safari"]),
html[data-rn-plat="firefox"]  .get-app-platform[data-plat]:not([data-plat~="firefox"]){ display: none; }
/* Firefox can't install a PWA → the scan-to-phone QR isn't the path; the Firefox
   card already points to the extension. */
html[data-rn-plat="firefox"] .get-app-qr-card{ display: none; }

/* Dashboard "Install the app" — an edge-accent ACTION card (carries .edge-accent
   .edge-accent--muted in the markup): muted grey edge at rest → tier edge + soft
   tier tint on hover, the glyph lighting tier too. It's a single CTA / point of
   truth, distinct from the multi-step helper guides below (filled step tiles), and
   it mirrors the Connect card geometry (52px, 8px 14px) so the two "Get started"
   items read as a matched pair. Whole card links to /get-the-app. */
.dashboard-get-app{ display: flex; align-items: center; gap: 12px; width: 100%;
  min-height: 52px; padding: 8px 14px; box-sizing: border-box; text-decoration: none; cursor: pointer; }
.dashboard-get-app:hover{ border-left-color: var(--tier-accent, var(--accent-blue));
  background: color-mix(in srgb, var(--tier-accent, var(--accent-blue)) 8%, transparent); }
.dashboard-get-app-icon{ flex-shrink: 0; display: inline-flex; color: var(--text-preview);
  transition: color 140ms ease; }
/* On hover the QR glyph brightens to --text (neutral), NOT the tier accent — the
   edge + tint already carry the tier signal. */
.dashboard-get-app:hover .dashboard-get-app-icon{ color: var(--text); }
.dashboard-get-app-glyph{ display: block; }
.dashboard-get-app-body{ display: flex; flex-direction: column; gap: 2px; flex: 1 1 auto; min-width: 0; }
.dashboard-get-app-title{ font-weight: 500; color: var(--text-preview); transition: color 140ms ease; }
.dashboard-get-app:hover .dashboard-get-app-title{ color: var(--text); }
.dashboard-get-app-sub{ font-size: var(--type-meta-size); color: var(--text-meta); line-height: 1.4;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
/* Bare arrow, matching the Connect card's .ig-setup-arrow (tier, 1.05rem). */
.dashboard-get-app-cta{ flex-shrink: 0; color: var(--tier-accent, var(--accent-blue));
  font-size: 1.05rem; white-space: nowrap; }

/* "Get started" — Connect + Install side by side on wide. auto-fit so a lone card
   (once IG is connected) fills the row instead of orphaning at half-width; both
   cells stretch to equal height. Connect (rendered first) sits on the LHS. On
   narrow the grid reflows to one column and the two cards stack — the install
   card uses a QR glyph (not a scannable QR), so it stays at every width. */
.dashboard-startup-grid{ display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 8px; align-items: stretch; }
/* Inside an installed PWA, "Install the app" is nonsensical — hide it (no flash). */
@media (display-mode: standalone){ .dashboard-startup-install{ display: none; } }
/* Stretched-link overlay — the whole card navigates; sits BELOW the dismiss ×. */
.dashboard-get-app-stretch{ position: absolute; inset: 0; z-index: 1; border-radius: inherit; }
/* Title + dismiss × on one inline row, so the × sits adjacent-right of the label
   (not at the far edge by the arrow CTA). */
.dashboard-get-app-titlerow{ display: inline-flex; align-items: center; gap: 6px; }
/* Hover-revealed dismiss × — its OWN centred circular hover background (overrides
   the global button:hover lozenge), the glyph flex-centred in it. Above the
   stretch link (z-index) so it stays clickable. Paint-only reveal (hover canon). */
.dashboard-getapp-dismiss{
  position: relative; z-index: 2; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; padding: 0;
  border: 0; border-radius: 50%; background: none; cursor: pointer;
  color: var(--text-meta); font-size: 14px; line-height: 1;
  opacity: 0; transition: opacity 140ms ease, color 140ms ease, background 140ms ease;
}
.dashboard-startup-install:hover .dashboard-getapp-dismiss{ opacity: 1; }
.dashboard-getapp-dismiss:hover{ color: var(--text); background: var(--btn-hover); }
.dashboard-getapp-dismiss:focus-visible{ opacity: 1; }
/* Matched-pair glyph behaviour: both GET STARTED cards' glyphs read --text-preview
   at rest and brighten to --text on hover. The IG glyph (shared ig_connect_card
   macro) is full-bright at rest elsewhere, so scope the muted-rest treatment here. */
.dashboard-startup .ig-setup-icon{ color: var(--text-preview); transition: color 140ms ease; }
.dashboard-startup .ig-setup-card:hover .ig-setup-icon{ color: var(--text); }

/* ============================================================
   PWA launch splash — standalone-only, first-of-session. Markup:
   partials/_pwa_splash.html; gate: base.html head sets html.splash-active.
   Hidden by default → zero impact on the normal browser experience.
   ============================================================ */
.pwa-splash{ display: none; }
html.splash-active .pwa-splash{
  display: flex;
  position: fixed;
  inset: 0;
  z-index: 9999;
  align-items: center;
  justify-content: center;
  /* Bias the wordmark slightly ABOVE dead-center so it lands at the same height as
     the share-confirmation card that follows it in the launch sequence (that card
     sits above true center — it's an 80vh block at the top of .main + a bottom-pad
     bias). box-sizing keeps the padding inside the fixed inset:0 box (no overflow). */
  box-sizing: border-box;
  padding-bottom: min(8vh, 72px);
  background: #061020;   /* matches the manifest splash background_color */
  /* Hold the splash FULLY OPAQUE to a fixed time, then HARD-CUT to the app (1ms =
     instant) — no opacity cross-fade, so no smudge/blend between splash and app. */
  animation: pwa-splash-out 1ms linear 1500ms forwards;
}
.pwa-splash-mark{ width: min(62vw, 280px); height: auto; color: #fff;
  /* Descenders sit at the viewBox bottom edge (y≈358 in a 358-tall box); without
     this the SVG viewport shaves the bottom pixel row (flattened letter bottoms),
     and the scale-in could nick edges too. */
  overflow: visible; }
/* Letters grow from their own centre, staggered L→R via inline animation-delay. */
html.splash-active .pwa-splash-letter{
  transform-box: fill-box; transform-origin: center; opacity: 0;
  animation: pwa-splash-letter 340ms cubic-bezier(.42, 0, .58, 1) both;
}
/* The triangle (brand 'o') rotates in and settles to the play position. */
html.splash-active .pwa-splash-tri{
  transform-box: fill-box; transform-origin: center; opacity: 0;
  animation: pwa-splash-tri 680ms cubic-bezier(.34, 1.4, .5, 1) both;
}
@keyframes pwa-splash-letter{ from{ opacity: 0; transform: scale(.32); } to{ opacity: 1; transform: scale(1); } }
@keyframes pwa-splash-tri{
  from{ opacity: 0; transform: rotate(-360deg) scale(.4); }
  60%{ opacity: 1; }
  to{ opacity: 1; transform: rotate(0deg) scale(1); }
}
@keyframes pwa-splash-out{ from{ opacity: 1; } to{ opacity: 0; visibility: hidden; pointer-events: none; } }
/* Reduced motion: no grow/spin — static wordmark, quick fade. */
@media (prefers-reduced-motion: reduce){
  html.splash-active .pwa-splash-letter,
  html.splash-active .pwa-splash-tri{ animation: none; opacity: 1; transform: none; }
  html.splash-active .pwa-splash{ animation: pwa-splash-out 1ms linear 700ms forwards; }
}

/* ============================================================
   Quick-start walkthrough overlay (opt-in onboarding) — full-screen takeover
   hosting the self-contained /quickstart walkthrough iframe. See base.html.
   ============================================================ */
.quickstart-overlay{ position: fixed; inset: 0; z-index: 1000; background: #061020; }
/* Light theme: the backdrop behind the guide iframe matches the app's light bg
   (the iframe itself is themed via /quickstart?theme=light). */
html[data-theme="light"] .quickstart-overlay{ background: #E6E8EB; }
.quickstart-overlay[hidden]{ display: none; }
.quickstart-frame{ width: 100%; height: 100%; border: 0; display: block; background: #061020; }
/* Theme-match the iframe element so there's no white flash before its document
   paints (the loading iframe defaults to white). */
html[data-theme="light"] .quickstart-frame{ background: #E6E8EB; }
.quickstart-close{
  position: fixed; top: max(10px, env(safe-area-inset-top)); right: 14px; z-index: 1001;
  width: 34px; height: 34px; display: inline-flex; align-items: center; justify-content: center;
  border: 0.5px solid rgba(255,255,255,0.14); border-radius: 50%;
  background: rgba(255,255,255,0.06); color: #E8EEF5; font-size: 20px; line-height: 1;
  cursor: pointer; transition: background 140ms ease;
}
.quickstart-close:hover{ background: rgba(255,255,255,0.12); }
/* Light theme: the close button is white-on-dark by default → invisible on the
   light backdrop. Flip it to dark-on-light. */
html[data-theme="light"] .quickstart-close{
  border-color: rgba(0,0,0,0.18); background: rgba(0,0,0,0.05); color: #2A2D31;
}
html[data-theme="light"] .quickstart-close:hover{ background: rgba(0,0,0,0.10); }
/* Block-level so the cross has no inline-baseline gap → dead-centre in its circle. */
.quickstart-close svg{ display: block; }
/* "New here? walkthrough" launcher under the Get-started cards — tier-coloured,
   brightens on hover (matches the how-to-save "Let's go" links). */
/* "Watch the get started guide" — a TIER-COLOURED camera button adjacent to the GET
   STARTED title (clustered LEFT, not far-right on the eye-icon line: it's a different
   function and shouldn't read as a "view all" or compete with the connect card's CTA
   arrow below). The guide is also permanently in the More menu. */
.dashboard-startup .dashboard-section-head{ justify-content: flex-start; }
.dashboard-startup-watch{
  display: inline-flex; align-items: center;
  background: none; border: none; padding: 2px; cursor: pointer;
  color: var(--tier-accent, #0698ea);
  transition: color 140ms ease;
  -webkit-tap-highlight-color: transparent;
}
/* background:none also on hover — overrides the global button:hover{ background:
   var(--btn-hover) }, which was painting a pill behind the bare camera. */
.dashboard-startup-watch:hover{ color: var(--text); background: none; }
/* No focus ring on click (the button stays focused after opening the overlay → a
   flattened oval reads as an unwanted background); keep a ring for keyboard a11y. */
.dashboard-startup-watch:focus{ outline: none; }
.dashboard-startup-watch:focus-visible{ outline: 2px solid var(--tier-accent, #0698ea); outline-offset: 2px; border-radius: 4px; }
.dashboard-startup-watch svg{ height: 11px; width: auto; display: block; }

/* ── Legal documents (Privacy Policy / Terms) ─────────────────────────────────
   Readable long-form prose inside the standard content card. */
/* Fill the responsive content column (980px, shrinks on narrow) rather than a
   fixed 760px block — full-width like the other content surfaces. */
.legal-doc{ max-width: 100%; }
.legal-doc h1{ font-size: 1.4rem; margin: 0; color: var(--text); }
.legal-doc .legal-meta{ color: var(--text-meta); font-size: 0.82rem; margin: 4px 0 4px; }
.legal-doc .legal-intro{ color: var(--text-preview); line-height: 1.55; font-size: var(--type-body-size); margin: 8px 0 0; }
.legal-doc h2{ font-size: 1.04rem; margin: 24px 0 6px; color: var(--text); }
.legal-doc h3{ font-size: 0.94rem; margin: 14px 0 4px; color: var(--text); }
.legal-doc p, .legal-doc li{ color: var(--text-preview); line-height: 1.55; font-size: var(--type-body-size); }
.legal-doc p{ margin: 6px 0; }
.legal-doc ul{ margin: 6px 0; padding-left: 20px; }
.legal-doc li{ margin: 4px 0; }
.legal-doc a{ color: var(--accent-blue); }
.legal-doc strong{ color: var(--text); font-weight: 600; }
.legal-doc table{ width: 100%; border-collapse: collapse; margin: 10px 0; font-size: var(--type-body-size); }
.legal-doc th, .legal-doc td{ text-align: left; padding: 6px 8px; border-bottom: 0.5px solid var(--border); color: var(--text-preview); vertical-align: top; }
.legal-doc th{ color: var(--text); font-weight: 600; }

/* Sign-up consent line — links Terms + Privacy under the Create-account button. */
.auth-consent{
  margin: 12px 0 0;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--text-meta);
  text-align: center;
}
.auth-consent a{ color: var(--accent-blue); }

/* ── Thumbnail smart-crop Phase 1: upward focal bias ───────────────────────
   Every reel-frame thumbnail img (object-fit:cover) shares one upward crop
   bias so a portrait subject's face/head stays in frame across all box
   aspects. See the --thumb-focal-y note in :root. The audio-icon variant
   (.thumb-audio) is object-fit:contain and is reset to centre below. */
.channel-mosaic-tile,
.channel-list-thumb,
.media-thumb img,
.recent-save-thumb img,
.save-hero__thumb img,
.library-carousel-thumb img,
.trial-expired-mosaic-tile img{
  object-position: var(--thumb-focal-x, 50%) var(--thumb-focal-y);
}
/* Audio placeholder icon — keep it dead-centre, not biased. */
.media-thumb.thumb-audio img{
  object-position: 50% 50%;
}

/* ── Embed mode — /share/{public_id}?embed=1 ─────────────────────────────────
   Chrome-less variant for cross-origin iframe embedding (the marketing-site
   demo). The anon-viewer nav is already gone (base.html gates it on
   chrome_context); this additionally HIDES THE SCROLLBAR (so the marketing side
   can drop its cross-origin scrollbar workaround), drops the nav offsets, and
   trims the page gutters so the Card fills the frame. All scoped to body.is-embed
   — the normal public share page is untouched. */
/* `is-embed` = chrome-less layout (both embed modes). `is-embed--bare` ALSO hides the
   scrollbar — that's the internal marketing demo (?embed=1). The user-facing
   ?embed=card keeps a VISIBLE scrollbar so tall Cards aren't clipped on a host page. */
body.is-embed--bare{ scrollbar-width: none; -ms-overflow-style: none; }
body.is-embed--bare::-webkit-scrollbar{ width: 0; height: 0; display: none; }
/* WebKit propagates `body{overflow-y:scroll}` to the VIEWPORT, whose scrollbar is
   NOT styled by body::-webkit-scrollbar — so a classic/WebKit scrollbar still shows.
   Also hide it via the root element to fully clear the bare-demo embed. */
html:has(body.is-embed--bare){ scrollbar-width: none; }
html:has(body.is-embed--bare)::-webkit-scrollbar{ width: 0; height: 0; display: none; }
body.is-embed .side-rail,
body.is-embed .tab-bar{ display: none !important; }
body.is-embed .wrap{ min-height: 0; padding-left: 0 !important; padding-bottom: 0 !important; }
body.is-embed .main{ max-width: none; padding: 12px; }
/* Always-on ReelNotes header on an embedded Card — mirrors .top-strip (wordmark +
   search-styled bar) so every embed is branded for every viewer, not just a
   logged-in same-site one. The search bar is presentational (no input). */
body.is-embed .embed-header{
  display: flex; flex-wrap: nowrap; align-items: center; gap: 16px;
  margin-bottom: 16px; padding-bottom: 14px;
  border-bottom: 1px solid var(--border);
}
body.is-embed .embed-header-brand{
  display: inline-flex; align-items: center; flex-shrink: 0;
  text-decoration: none; color: var(--text);
  transition: opacity 120ms ease;
}
body.is-embed .embed-header-brand .brand-logo{ width: 128px; height: auto; display: block; }
body.is-embed .embed-header-brand:hover{ opacity: 0.85; }
body.is-embed .embed-header-search{
  display: inline-flex; align-items: center; gap: 2px;
  flex: 1 1 auto; min-width: 5.5rem; height: 28px; padding: 0 10px;
  background: var(--surface-2); border: 0.5px solid var(--border);
  border-radius: 999px; color: var(--text-meta); font-size: 0.75rem;
}
body.is-embed .embed-header-search-icon{ width: 16px; height: 16px; flex-shrink: 0; }
/* Attributed embed (?embed=card) backlink — small, centred under the card. */
body.is-embed .embed-attribution{ display: block; margin: 8px 0 4px; text-align: center; font-size: 0.8125rem; color: var(--text-meta); text-decoration: none; }
body.is-embed .embed-attribution:hover{ color: #0698ea; }
/* Embed-code snippet field in the "Embed this Card" modal (_embed_share_modal.html). */
.embed-snippet-field{
  width: 100%; box-sizing: border-box; resize: none;
  font-family: ui-monospace, "SFMono-Regular", "Courier New", monospace;
  font-size: 0.8125rem; line-height: 1.5;
  color: var(--text); background: var(--surface-2);
  border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px;
}

