/* ── Accessibility utilities ─────────────────────────────────────────
   Skip link — first focusable element on every page. Hidden visually
   until keyboard focus brings it forward. Required for WCAG 2.4.1. */
.skip-link {
  position: fixed;
  top: -100px;
  left: 8px;
  z-index: 99999;
  background: var(--bs-primary);
  color: var(--pp-on-primary, #fff);
  padding: 10px 16px;
  border-radius: 6px;
  text-decoration: none;
  font-weight: 600;
  transition: top .15s;
}
.skip-link:focus { top: 8px; outline: 2px solid var(--bs-emphasis-color); outline-offset: 2px; }

/* Global focus-visible — every focusable element gets a clearly visible
   indicator. Project-wide fallback so any element that strips Bootstrap's
   default still has accessible focus. Required for WCAG 2.4.7 + 1.4.11. */
:focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: 2px;
}
/* Form-control focus: use Bootstrap's box-shadow ring instead of an outline
   since the ring works with rounded corners and doesn't shift layout. */
.form-control:focus-visible,
.form-select:focus-visible,
.form-check-input:focus-visible {
  outline: none;
  border-color: var(--bs-primary);
  box-shadow: 0 0 0 3px rgba(var(--bs-primary-rgb), .35);
}

/* Empty live-region alerts must remain in the DOM so screen readers see
   them as persistent regions; visually hide them when they have no text. */
.alert:empty { display: none !important; padding: 0; border: 0; margin: 0; }

:root {
  --brand: #0f172a;  /* login gradient */
  --gold:  #d97706;  /* warm accent (no Bootstrap equiv) */
  --pink:  #e879f9;  /* fuchsia accent (no Bootstrap equiv) */
  --bs-body-font-family: 'DM Sans', sans-serif;
  --bs-btn-font-family:  'DM Sans', sans-serif;
  --bs-btn-font-weight:  600;
  --bs-btn-font-size:    .875rem;
  /* Semantic calendar event colors — WCAG AA against the body bg.
     Presets can override per-theme via these same names. */
  --pp-accent-party: var(--bs-primary);
  --pp-accent-order: #d97706;     /* amber-600  — 5.5:1 on white, 5.5:1 on #212529 */
  --pp-accent-ann:   #059669;     /* emerald-600 — 4.5:1 on white, 4.6:1 on #212529 */
  /* Smart text-on-bg colors — pick white or dark per background luminance so
     contrast stays AA when accents are themed or in dark mode. theme.js
     overrides --pp-on-primary per preset; the accents below are amber/emerald
     family (mid-luminance) so dark text is the right answer in both modes. */
  --pp-on-primary:      #fff;     /* default for dark-ish Bootstrap primary #0d6efd */
  --pp-on-accent-order: #1a0f00;  /* dark text — 5.1:1 on amber-600, 10.5:1 on amber-400 */
  --pp-on-accent-ann:   #052e1a;  /* dark text — 5.0:1 on emerald-600, 10.5:1 on emerald-400 */
}

/* ── Premium base polish ─────────────────────────────────────────
   The base design layer. Jeff: the portal "feels a little cheap … maybe the border
   lines are making it feel dated." Bootstrap's default border grays (#dee2e6
   light / #495057 dark) are the harshest lines in the app — soften them
   globally (every divider, card outline, and table rule references the var),
   then add quiet depth cues so structure comes from elevation, not lines.
   Form controls are NOT washed out by this: they keep their own explicit
   WCAG 3:1 border override (see the form-control block further down). */
:root {
  --bs-border-color: #e3e8ef;                        /* slate-tinted, softer than #dee2e6 */
  --bs-border-color-translucent: rgba(15,23,42,.07);
  --pp-scrollbar: rgba(15,23,42,.22);
}
/* Dark palette — Bootstrap's stock dark (#212529 body / #2b3035 tertiary /
   #495057 borders) is a flat neutral gray that reads cheap. This is a
   slate-tinted ladder with real separation between the three depths:
   page #0e1117  <  panels #161a21  <  elevated chrome #1e232c.
   ⚠ --bs-body-bg IS the surface token (cards/modals/sidebar/table cells all
   paint it) — the PAGE tone is --pp-page-bg, applied to body only. */
[data-bs-theme="dark"] {
  --bs-body-bg:         #161a21;
  --bs-body-bg-rgb:     22,26,33;                    /* keeps rgba() scrims (fcal overlay) in sync */
  --bs-tertiary-bg:     #1e232c;                     /* navbar, sidebar, toolbars, subtle fills */
  --bs-secondary-bg:    #262c36;                     /* one step deeper — hover on tertiary */
  --bs-body-color:      #d6dce4;
  --bs-emphasis-color:  #f1f5f9;
  --bs-secondary-color: #98a2b3;                     /* 6.8:1 on panels — Bootstrap's is dimmer */
  --bs-border-color: #29303a;
  --bs-border-color-translucent: rgba(255,255,255,.07);
  --pp-scrollbar: rgba(255,255,255,.22);
}
/* Depth tied to the admin-chosen shadow tier (vibe "flat" → none, unchanged) */
.card { box-shadow: var(--bs-box-shadow-sm); }
.dropdown-menu { box-shadow: var(--bs-box-shadow); }
.modal-content { box-shadow: var(--bs-box-shadow-lg); }
/* Top chrome floats over the work area instead of sitting behind a hard rule */
.navbar { box-shadow: 0 1px 0 var(--bs-border-color-translucent), 0 4px 12px rgba(15,23,42,.04); }
[data-bs-theme="dark"] .navbar { box-shadow: 0 1px 0 rgba(0,0,0,.35), 0 4px 12px rgba(0,0,0,.28); }
/* Focus ring follows the theme primary (Bootstrap ships a hardcoded blue ring,
   which clashes under non-blue vibes like the live slate) — and is a touch
   quieter than the stock .25rem/.25 flood. */
.form-control:focus, .form-select:focus {
  border-color: rgba(var(--bs-primary-rgb), .55);
  box-shadow: 0 0 0 .2rem rgba(var(--bs-primary-rgb), .14);
}
/* Slim, quiet scrollbars — the chunky OS default (esp. Windows) reads as
   unfinished. Standard props cover Chrome/Firefox; the -webkit rules are the
   Safari fallback (ignored by browsers honoring scrollbar-width). */
* { scrollbar-width: thin; scrollbar-color: var(--pp-scrollbar) transparent; }
*::-webkit-scrollbar { width: 9px; height: 9px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: var(--pp-scrollbar); border-radius: 999px; }
/* Headlines: a whisper of negative tracking reads more set, less default */
h1, h2, h3, .h1, .h2, .h3 { letter-spacing: -.015em; }

/* Theme crossfade (View Transitions): kill every element's own transition for
   the duration so nothing tweens on top of the cross-dissolve and lags it. */
html[data-vt-freeze] *, html[data-vt-freeze] *::before, html[data-vt-freeze] *::after {
  transition: none !important;
}
::view-transition-old(root), ::view-transition-new(root) { animation-duration: .32s; }

/* ── Aether pass: airy page-below-panels + techy details ─────────
   Jeff: "airier and techy … a premium experience." The page (body) drops a
   half-step below the panels, so every surface that paints var(--bs-body-bg) —
   cards, sidebar, tables, modals, sticky masks — floats for free.
   ⚠ NEVER retint --bs-body-bg itself: Bootstrap derives card/modal/dropdown/
   offcanvas backgrounds from it, and ~120 portal rules use it as the
   "surface white" token. The tint must stay a literal body paint. */
:root { --pp-page-bg: #f5f7fa; }
[data-bs-theme="dark"] { --pp-page-bg: #0e1117; }
body { background-color: var(--pp-page-bg); }
/* ── Harmony tokens (Pass 1) ─────────────────────────────────────
   Radius tiers — reach for these, never a fresh literal px:
     sm    = var(--bs-border-radius-sm)  → chips, icon buttons, thumbnails, dense hover rows
     base  = var(--bs-border-radius)     → buttons, inputs, segmented controls, dropdowns, in-feed media
     lg    = var(--bs-border-radius-lg)  → cards, panels, modals, sheets
     999px = pills · 50% = circles · 20px = login hero / mobile bottom-sheet ONLY
     (2–4px micro family stays literal: inline code/kbd, cmdk-hl, calendar chips)
   Borders: var(--bs-border-width) solid/dashed (1px) — 2px is reserved for
   circular radio indicators and large dashed drop-zones.
   Hover-chrome transitions: use --pp-t (background-color/color/border-color/
   box-shadow only — motion transitions keep their own timing).
   Micro-labels: .pp-eyebrow is the one uppercase eyebrow recipe. */
:root { --pp-t: .15s ease; }  /* shared hover-chrome transition timing */
/* Bootstrap's .text-secondary utility hardcodes #6c757d in both modes (~2.9:1
   in dark) — remap the ~189 markup uses to the adaptive gray token. */
.text-secondary { color: var(--bs-secondary-color) !important; }
/* Micro-label eyebrow — the single recipe for uppercase section labels. */
.pp-eyebrow { font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--bs-secondary-color); }
/* The top chrome has no background of its own (it inherited the old white
   body) — pin it to the panel tone so it reads as chrome, not page. Dark uses
   tertiary to match the dark sidebar's elevated-chrome tone (theme.js's dark
   meta theme-color tracks this). */
.navbar { background: var(--bs-body-bg); }
[data-bs-theme="dark"] .navbar { background: var(--bs-tertiary-bg); }
/* With the page a half-step down, background contrast draws the chrome
   boundary — the hard rule can go (the Bootstrap utility is !important). */
.navbar.border-bottom { border-bottom-color: transparent !important; }
/* Full-page calendar: float the sheet like a card. bg + radius + shadow ONLY —
   no overflow:hidden (FC positions its +N-more popovers inside .fc). */
#fullCalGrid .fc { background: var(--bs-body-bg); border-radius: var(--bs-border-radius-lg); box-shadow: var(--bs-box-shadow-sm); }
/* FC's own scrollgrid border is square-cornered and would poke through the
   sheet's bottom radius — round the border itself (no clipping involved). */
#fullCalGrid .fc-scrollgrid { border-radius: 0 0 var(--bs-border-radius-lg) var(--bs-border-radius-lg); }
/* Dashboard/calendar column splits: the tint separates now; drop the
   full-height rules (utilities are !important). */
#welcomeState .border-end, #fullCalendarManager .border-end,
#myDetailsManager .border-end { border-inline-end-color: transparent !important; }
/* Kill the last stock-Bootstrap-blue leaks — compiled colors the vibe's
   primary never reaches (a "cheap" tell under the non-blue live theme). */
.form-check-input:checked { background-color: var(--bs-primary); border-color: var(--bs-primary); }
/* :indeterminate is compiled to literal blue too, and it's live (the affiliates
   select-all header checkbox); :focus (mouse) likewise — only :focus-visible
   was themed before, so clicking a checkbox flashed a stock-blue halo. */
.form-check-input[type=checkbox]:indeterminate { background-color: var(--bs-primary); border-color: var(--bs-primary); }
.form-check-input:focus { border-color: rgba(var(--bs-primary-rgb), .55); box-shadow: 0 0 0 .2rem rgba(var(--bs-primary-rgb), .14); }
.btn-primary, .btn-outline-primary { --bs-btn-focus-shadow-rgb: var(--bs-primary-rgb); }
/* Numbers line up everywhere (DM Sans ships the tnum feature) */
td, th, .badge { font-variant-numeric: tabular-nums; }
/* Dark cards get a hairline top-sheen — reads as a machined edge, not a line */
[data-bs-theme="dark"] .card { box-shadow: inset 0 1px 0 rgba(255,255,255,.05), var(--bs-box-shadow-sm); }
/* thead.table-light hardcodes a light-gray bar into dark mode (KB tables +
   import-preview modal) — make it theme-aware. The border var matters too:
   .table-light compiles --bs-table-border-color:#c6c7c8, which would draw
   glowing light hairlines around the now-dark header cells. */
[data-bs-theme="dark"] .table-light { --bs-table-bg: var(--bs-tertiary-bg); --bs-table-color: var(--bs-body-color); --bs-table-border-color: var(--bs-border-color); }

* { box-sizing: border-box; }
/* Lock the page to the viewport: no scrolling above the topbar, no rubber-band
   bounce when scrolling already-pinned content. iOS Safari especially likes to
   rubber-band the html element even when body is overflow:hidden — overscroll-
   behavior on both html and body kills that. height:100% + position:fixed on
   html guarantees no chrome-scroll ever exposes white space above the topbar. */
html, body {
  overscroll-behavior: none;
  height: 100%;
  overflow: hidden;
}
html { position: fixed; inset: 0; width: 100%; }
body { font-family: 'DM Sans', sans-serif; height: 100%; overflow: hidden; }
/* The CANVAS (safe-area strips, overscroll gutters, anything outside the body
   box) paints the <html> background — not body's. Without this it falls back
   to the UA default, which is why the iOS safe areas kept the OLD mode's color
   after a light/dark toggle while the drawer was open (Jeff): nothing
   theme-reactive was painting them. Same token as body so they always match. */
html { background-color: var(--pp-page-bg); }
/* Bootstrap's modal/offcanvas scrim is a hardcoded #000 that never follows the
   theme. iOS 26/27 Safari samples fixed overlays for its toolbar + safe-area
   tint, so every modal (changelog, print-job, shipping…) painted the safe areas
   with a colour that couldn't change when you toggled light/dark. Modals must
   stay position:fixed, so the fix is to make the sampled colour theme-derived:
   the bars still go solid while a modal is open, but they're the RIGHT solid
   and they retint with the mode. */
:root { --bs-backdrop-bg: #0f172a; --bs-backdrop-opacity: .45; }
[data-bs-theme="dark"] { --bs-backdrop-bg: #05070b; --bs-backdrop-opacity: .6; }

/* ── Mobile: edge-to-edge, content scrolls behind the browser chrome ──
   iOS 18/26+ (and Chrome Android) only blend/collapse their toolbars for the
   ROOT scroller. The fixed-shell lock above makes an inner div the scroller,
   so the chrome stays an opaque band. On phones/tablets we hand scrolling back
   to the document: the strip sticks under the (now translucent) top chrome and
   content runs under both bars, with safe-area padding so nothing is trapped
   beneath them. Desktop keeps the fixed shell. Paired with viewport-fit=cover
   and no theme-color meta (index.html). */
@media (max-width: 991.98px) {
  html { position: static; inset: auto; height: auto; overflow: visible; overscroll-behavior: auto; }
  body { height: auto; min-height: 100dvh; overflow: visible; overscroll-behavior: auto; }
  #appShell { height: auto !important; min-height: 100dvh; max-width: 100%; }
  /* overflow-x MUST stay clipped — plain `overflow: visible` here let wide
     content (manager tables) push the document sideways on iPhone (Jeff).
     `clip` rather than `hidden`: hidden on one axis forces the other to
     `auto`, which would recreate a scroll container and kill the root
     scrolling the translucent chrome depends on. */
  html, body { overflow-x: clip; max-width: 100%; }
  /* ⚠ min-width:0 is LOAD-BEARING, not belt-and-braces. A flex item's default
     `min-width:auto` refuses to shrink below its content's min-content width —
     unless the item has non-visible overflow, which zeroes that automatic
     minimum. #mainPanel used to be `overflow:auto`, so it shrank correctly.
     Switching it to `overflow-x:clip; overflow-y:visible` for root scrolling
     re-armed the automatic minimum and the panel inflated to its content's
     960px inside a 430px iPhone viewport, dragging every descendant sideways
     with it (measured: 1867 overflowing elements -> 6 with this line). */
  #mainPanel, #appShell > .d-flex.flex-grow-1 { min-width: 0; }
  #mainPanel { overflow-x: clip !important; overflow-y: visible !important; }
  .pp-mobilebar {
    position: sticky; top: 0; z-index: 1035;
    padding-top: env(safe-area-inset-top);
    min-height: calc(48px + env(safe-area-inset-top));
  }
  /* Last content gets clearance from the bottom toolbar / home indicator. */
  #mainPanel { padding-bottom: calc(env(safe-area-inset-bottom) + 1.25rem); }
  /* The slide-in calendar drawers are fixed to the viewport, so they need the
     inset themselves — their top:48px assumed a non-inset bar. */
  #dashCalSidebar, #ctxCalSidebar { top: calc(48px + env(safe-area-inset-top)); }
}

/* ── Login screen ── */
#loginScreen {
  position: fixed; inset: 0; background: linear-gradient(135deg, var(--brand) 0%, #0f2456 100%);
  display: flex; align-items: center; justify-content: center; z-index: 9999;
}
.login-card {
  background: var(--bs-body-bg); border-radius: 20px; padding: 2.75rem 2.25rem;
  width: 100%; max-width: 400px; box-shadow: 0 20px 60px rgba(0,0,0,.3);
}
.login-card img { height: 34px; margin-bottom: 1.75rem; }
.login-card h2 { font-family: 'Comfortaa', sans-serif; font-size: 1.5rem; color: var(--bs-emphasis-color); margin-bottom: .25rem; }
.login-card p  { color: var(--bs-secondary-color); font-size: .9rem; margin-bottom: 1.75rem; }

/* Hide the login form the instant we know a session exists (html.pp-authed is set pre-paint in
   index.html from the saved token), so a logged-in user NEVER sees sign-in. The portal then paints
   from the cached profile (boot() in portal.js). Cleared on logout / 401, so login shows then. */
html.pp-authed #loginScreen { display: none !important; }

/* ── Topbar elements ── */
/* ── Shell rebuild: mobile strip + sidebar chrome ──
   Desktop has NO top bar — the sidebar is the whole chrome (brand/home,
   search, nav, theme switch, logout). Mobile gets a 48px strip. */
.pp-mobilebar { min-height: 48px; }
.pp-strip-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; flex-shrink: 0;
  border: none; background: none; color: var(--bs-secondary-color);
  border-radius: var(--bs-border-radius-sm); font-size: .95rem;
  transition: background-color var(--pp-t), color var(--pp-t);
}
.pp-strip-btn:hover, .pp-strip-btn:active { background: var(--bs-tertiary-bg); color: var(--bs-emphasis-color); }
.pp-strip-title {
  font-size: .95rem; font-weight: 700; color: var(--bs-emphasis-color);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
  margin-left: .2rem;
}
.pp-strip-count { flex-shrink: 0; font-size: .68rem; font-weight: 700; }
/* Sidebar identity lives in the FOOTER now (logo + "Affiliate Portal" +
   version + the collapse control); the profile card is the top of the rail. */
.sidebar-brand-name {   /* mobile drawer header label only */
  font-family: 'Comfortaa', sans-serif; font-weight: 700; font-size: .92rem;
  color: var(--bs-emphasis-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Nav rows are 1.25rem of padding behind a 3px active border, so their text
   starts at 23px. The footer and the logout row share that edge — anything
   else reads as a misalignment down the left side of the rail. */
:root { --pp-rail-x: calc(1.25rem + 3px); }
/* Four items in a 240px rail is tight: at 1rem padding + .6rem gaps the
   identity block was squeezed to 69px, so "Affiliate Portal" wrapped and the
   footer grew to 74px. Trim the ornament (smaller logo, tighter gaps, "v"
   instead of the word "version") and the row fits on one line. */
/* A bigger logo costs the identity block width in a 240px rail, so the gaps
   and the right padding give the difference back — "Affiliate Portal" needs
   ~84px and clips the moment it drops below that. */
.sidebar-foot { display: flex; align-items: center; gap: .3rem; padding: .5rem .5rem .55rem var(--pp-rail-x); }
.sidebar-foot-logo { font-size: 1.5rem; color: var(--bs-primary); flex-shrink: 0; line-height: 1; }
.sidebar-foot-name {
  font-size: .7rem; font-weight: 700; color: var(--bs-body-color); line-height: 1.05;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sidebar-footer-text { min-width: 0; flex: 1 1 auto; line-height: 1.05; }
/* The version sits directly under the name — its own line-height was the only
   thing holding the two apart. */
.sidebar-footer-text .btn-version-link { display: block; line-height: 1.05; }
.sidebar-footer-text .btn-version-link { white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
/* Sidebar search pill — block-level version of the palette opener. Double-class
   specificity is REQUIRED: the base .pp-omnisearch rule is later in the file
   and its flex-basis:320px would otherwise win — in the sidebar's COLUMN flex
   that basis becomes HEIGHT (a 320px-tall pill; caught by the verify pass). */
.pp-omnisearch.pp-omnisearch--side { display: flex; flex: none; margin: .6rem .75rem .3rem; font-size: .84rem; padding: .38rem .7rem; }
.sidebar-admin-label { padding: .85rem 1.25rem .3rem; }
/* Sits in the footer beside the version identity. Spacing is MARGIN, never
   padding — the toggle is a fixed 46×24 border-box pill and inner padding
   would crush the knob's travel. */
.sidebar-theme-slot { flex-shrink: 0; align-self: center; margin-right: .1rem; }
/* Collapsed rail: logo-only brand (chevron beneath), no search, stacked bottom
   row. ⚠ MUST stay gated to desktop widths — the collapsed pref is server-
   synced and applied at every viewport, and the phone DRAWER is the same
   .sidebar element; ungated rules would strip the drawer (same failure mode
   the original collapsed-rail block documents). */
@media (min-width: 992px) {
  /* Collapsed rail shows ONLY the expand chevron up top — the logo is
     redundant next to a 56px rail of icons (Jeff). */
  /* Collapsed rail: only icons. Search, section label, footer identity and the
     theme toggle all fold away — the collapse chevron stays (Jeff). */
  .sidebar-collapsed .pp-omnisearch--side,
  .sidebar-collapsed .sidebar-admin-label,
  .sidebar-collapsed .sidebar-theme-slot,
  .sidebar-collapsed .sidebar-foot-logo { display: none; }
  .sidebar-collapsed .sidebar-foot { justify-content: center; padding: .5rem .25rem .55rem; }
}

/* ── Sidebar ── */
/* Single source of truth for the rail's paint — the scroll-shadow cover
   gradients have to match it exactly or they show up as a band. */
:root { --pp-sidebar-bg: var(--bs-body-bg); }
[data-bs-theme="dark"] { --pp-sidebar-bg: var(--bs-tertiary-bg); }
.sidebar {
  background: var(--pp-sidebar-bg);
  overflow: hidden;
  flex-shrink: 0;
  transition: width 0.3s ease, min-width 0.3s ease;
}
input[type="search"]::-webkit-search-cancel-button { display: none; }
.search-clear-btn {
  flex-shrink: 0; width: 24px; height: 24px; border: none; border-radius: 50%;
  background: var(--bs-tertiary-bg); cursor: pointer; color: var(--bs-secondary-color);
  display: flex; align-items: center; justify-content: center; font-size: .72rem;
  transition: background .15s; padding: 0;
}
.search-clear-btn:hover { background: var(--bs-border-color); color: var(--bs-emphasis-color); }
.cat-dropdown {
  position: absolute; top: calc(100% + 2px); left: 1rem; z-index: 200;
  background: var(--bs-body-bg); border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius);
  box-shadow: 0 4px 20px rgba(0,0,0,.12); min-width: 168px; overflow: hidden;
}
.cat-dropdown-item {
  padding: .55rem .9rem; font-size: .86rem; cursor: pointer;
  display: flex; align-items: center; gap: .5rem; color: var(--bs-body-color); transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t);
}
.cat-dropdown-item:hover { background: var(--bs-tertiary-bg); }
.cat-dropdown-item.active { color: var(--bs-primary); font-weight: 600; }
.cat-dropdown-item i { width: .9rem; text-align: center; font-size: .8rem; }
.client-list { padding: .5rem 0; }
.clients-section { flex: 1; overflow-y: auto; min-height: 0; }
.my-events-entry {
  display: flex; align-items: center; justify-content: space-between;
  padding: .6rem 1.25rem; cursor: pointer;
  /* reset native button chrome when element is a <button> */
  background: none; border: none; border-radius: 0; width: 100%; text-align: left;
  font-weight: 700; font-size: .78rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--bs-secondary-color); border-left: 3px solid transparent;
  transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t);
}
.my-events-entry:hover { background: var(--bs-tertiary-bg); color: var(--bs-emphasis-color); }
.my-events-entry.active { background: rgba(var(--bs-primary-rgb),.08); border-left-color: var(--bs-primary); color: var(--bs-primary); }
[data-bs-theme="dark"] .my-events-entry.active { background: rgba(var(--bs-primary-rgb),.22); color: color-mix(in srgb, var(--bs-primary) 60%, #fff); }
#adminSidebarSection .my-events-entry.active { background: rgba(220,38,38,.08); border-left-color: #dc2626; color: #dc2626; }
[data-bs-theme="dark"] #adminSidebarSection .my-events-entry.active { background: rgba(220,38,38,.22); color: #ff8a8a; }
.client-item {
  padding: .8rem 1.25rem; cursor: pointer;
  border-left: 3px solid transparent;
  transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t);
}
.client-item:hover { background: var(--bs-tertiary-bg); }
.client-item.active { background: rgba(var(--bs-primary-rgb),.08); border-left-color: var(--bs-primary); }
.client-name { font-weight: 600; font-size: .95rem; color: var(--bs-emphasis-color); }
.client-email { font-size: .82rem; color: var(--bs-secondary-color); margin-top: 2px; }
.cat-icon-chip {
  width: 34px; height: 34px; border-radius: var(--bs-border-radius-sm);
  background: var(--bs-tertiary-bg); color: var(--bs-secondary-color);
  display: flex; align-items: center; justify-content: center;
  font-size: .95rem; flex-shrink: 0;
}
.empty-clients {
  text-align: center; padding: 2rem 1.25rem; color: var(--bs-secondary-color); font-size: .85rem;
}

/* ── Context indicator bar ── */
/* ── Sidebar profile card ── */
.sidebar-profile {
  display: flex; align-items: center; gap: .7rem;
  padding: .95rem 1.1rem; border-bottom: 1px solid var(--bs-border-color);
  /* reset native button chrome when element is a <button> */
  border-top: none; border-right: none; border-radius: 0; width: 100%; text-align: left;
  background: var(--bs-tertiary-bg); flex-shrink: 0; min-width: 0;
}
.sidebar-profile-info { min-width: 0; flex: 1; }
.sidebar-profile-name { font-weight: 700; font-size: .875rem; color: var(--bs-emphasis-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.3; }
.sidebar-profile-company { font-size: .73rem; color: var(--bs-secondary-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.3; margin-top: .08rem; }
.sidebar-profile:hover { background: var(--bs-secondary-bg); }
/* A word, not a gear — the icon didn't say what tapping the row does (Jeff). */
.sidebar-profile-arrow {
  flex-shrink: 0; font-size: .62rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; color: var(--bs-secondary-color); opacity: .75;
}

/* ── Upcoming parties list ── */
.upcoming-party-row { display:flex; gap:.65rem; align-items:flex-start; padding:.45rem .55rem; border-radius:var(--bs-border-radius-sm); margin-bottom:2px; border-left:3px solid var(--bs-border-color); transition:background .12s; }
.upcoming-party-row:hover { background:var(--bs-tertiary-bg); }
.upcoming-party-row.status-upcoming { border-color:#15803d; }
.upcoming-party-row.status-today    { border-color:var(--bs-primary); }
.upcoming-party-row.status-past     { border-color:var(--bs-border-color); opacity:.7; }
.upcoming-date-block { display:flex; flex-direction:column; align-items:center; width:28px; flex-shrink:0; padding-top:.05rem; }
.upcoming-date-dow { font-size:.55rem; font-weight:700; text-transform:uppercase; letter-spacing:.04em; color:var(--bs-secondary-color); line-height:1; }
.upcoming-date-num { font-size:1rem; font-weight:700; line-height:1.1; color:var(--bs-emphasis-color); }
.upcoming-party-row.status-upcoming .upcoming-date-num { color:#15803d; }
.upcoming-party-row.status-today    .upcoming-date-num { color:var(--bs-primary); }
.upcoming-party-info { min-width:0; padding-top:.05rem; }
.upcoming-party-title { font-size:.82rem; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; color:var(--bs-emphasis-color); }
.upcoming-party-title a { color:inherit; text-decoration:none; }
.upcoming-party-title a:hover { color:var(--bs-primary); }
.upcoming-party-client { font-size:.72rem; color:var(--bs-secondary-color); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-top:1px; }
.upcoming-month-hdr { font-size:.7rem; text-transform:uppercase; letter-spacing:.06em; color:var(--bs-secondary-color); padding:.65rem .25rem .3rem; border-bottom:1px solid var(--bs-border-color); margin-bottom:.25rem; font-weight:700; }
.upcoming-month-hdr:first-child { padding-top:.1rem; }

/* ── Admin stats strip ──
   auto-fit wraps from 5-up on wide screens down to 2-up on tablets without
   needing a hand-tuned breakpoint per card-count. Mobile override below
   pins it to 2 columns with a sideways card layout. */
.stats-nums {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: .5rem; margin-bottom: .5rem;
}
.stats-charts {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: .5rem;
}
.stat-chart {
  background: var(--bs-body-bg); border: 1px solid var(--bs-border-color); border-radius: var(--bs-border-radius-lg);
  box-shadow: var(--bs-box-shadow-sm);
  padding: .55rem .75rem;
  display: flex; flex-direction: column; min-height: 110px;
}
.stat-chart-hdr {
  display: flex; align-items: center; justify-content: space-between; gap: .5rem;
}
.stat-chart-lbl {
  font-size: .62rem; color: var(--bs-secondary-color);
  text-transform: uppercase; letter-spacing: .04em; font-weight: 700;
  display: inline-flex; align-items: center; gap: .35rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.stat-chart-lbl i { color: var(--bs-primary); font-size: .68rem; }
.stat-chart-now {
  font-size: 1.05rem; font-weight: 700; color: var(--bs-emphasis-color);
  font-variant-numeric: tabular-nums; line-height: 1;
}
.stat-chart-spark {
  flex: 1; display: flex; align-items: stretch; margin: .35rem 0 .25rem;
  min-height: 38px;
}
.pp-sparkline { width: 100%; height: 100%; display: block; color: var(--bs-primary); }
.pp-sparkline-area { fill: currentColor; fill-opacity: .14; stroke: none; }
.pp-sparkline-line {
  fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke;
}
.stat-chart-sub {
  font-size: .67rem; color: var(--bs-secondary-color);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.stat-leaderboard { min-height: 140px; }
.stat-podium {
  flex: 1; display: grid; grid-template-columns: 1fr 1fr 1fr;
  align-items: end; gap: .35rem; margin-top: .35rem; padding-top: .25rem;
}
.podium-slot {
  display: flex; flex-direction: column; align-items: center;
  min-width: 0; cursor: pointer;
}
.podium-empty { cursor: default; opacity: .55; }
.podium-medal { font-size: 1rem; line-height: 1; margin-bottom: .15rem; }
.podium-1 .podium-medal { color: #f59e0b; font-size: 1.25rem; }
.podium-2 .podium-medal { color: #9ca3af; }
.podium-3 .podium-medal { color: #b45309; }
.podium-name {
  font-size: .7rem; font-weight: 600; color: var(--bs-emphasis-color);
  text-align: center; line-height: 1.15; max-width: 100%;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.podium-metric {
  font-size: .6rem; color: var(--bs-secondary-color);
  font-variant-numeric: tabular-nums; margin: .05rem 0 .2rem;
}
.podium-block {
  width: 100%; display: flex; align-items: center; justify-content: center;
  font-size: .9rem; font-weight: 800; color: #fff;
  border-radius: 4px 4px 0 0;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 1px 1px rgba(0,0,0,.18);
  box-shadow: inset 0 -2px 0 rgba(0,0,0,.12), 0 1px 0 rgba(0,0,0,.06);
}
.podium-1 .podium-block { height: 52px; background: linear-gradient(180deg,#fde68a,#f59e0b); }
.podium-2 .podium-block { height: 36px; background: linear-gradient(180deg,#e5e7eb,#9ca3af); }
.podium-3 .podium-block { height: 22px; background: linear-gradient(180deg,#fcd9b6,#b45309); }

/* Top-affiliates list — sits as a stat-chart tile alongside DAU / signups
   / orders, replacing the standalone podium widget. */
.top-aff-list { list-style: none; margin: .25rem 0 0; padding: 0; display: flex; flex-direction: column; gap: .2rem; }
.top-aff-list li {
  display: flex; gap: .4rem; align-items: baseline; min-width: 0;
  padding: .25rem .25rem; border-radius: .25rem; cursor: pointer;
  font-size: .8rem;
}
.top-aff-list li:hover { background: var(--bs-tertiary-bg); }
.top-aff-list li:hover .top-aff-name { color: var(--bs-primary); }
.top-aff-rank { color: var(--bs-secondary-color); font-variant-numeric: tabular-nums; font-weight: 600; width: 1.2em; }
.top-aff-name { font-weight: 600; color: var(--bs-emphasis-color); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.top-aff-metric { color: var(--bs-secondary-color); font-size: .7rem; font-variant-numeric: tabular-nums; }
.top-aff-empty { color: var(--bs-secondary-color); font-size: .75rem; padding: .35rem .25rem; cursor: default; list-style: none; }
.podium-empty .podium-block {
  background: var(--bs-tertiary-bg); color: var(--bs-secondary-color);
  box-shadow: inset 0 -2px 0 rgba(0,0,0,.04);
}
.podium-slot:not(.podium-empty):hover .podium-name { color: var(--bs-primary); }
.podium-slot:not(.podium-empty):hover .podium-block { filter: brightness(1.05); }
.stat-num {
  background: var(--bs-body-bg); border: 1px solid var(--bs-border-color); border-radius: var(--bs-border-radius-lg);
  box-shadow: var(--bs-box-shadow-sm);
  display: grid; grid-template-columns: auto 1fr; column-gap: .55rem; row-gap: 0;
  padding: .55rem .7rem; text-align: left;
}
.stat-num i {
  grid-row: 1 / 3; align-self: center;
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: var(--bs-border-radius-sm);
  background: rgba(var(--bs-primary-rgb),.1); color: var(--bs-primary); font-size: .72rem;
  margin-bottom: 0; flex-shrink: 0;
}
.stat-num-val { grid-column: 2; font-size: 1.15rem; font-weight: 700; color: var(--bs-emphasis-color); line-height: 1.1; }
.stat-num-lbl { grid-column: 2; font-size: .65rem; color: var(--bs-secondary-color); margin-top: .1rem; line-height: 1.2; }
/* Desktop layout already uses the compact horizontal-icon style by default;
   keep these breakpoints to tighten further and stay 2-col on narrow widths. */
@media (max-width: 900px) {
  .stats-nums { grid-template-columns: repeat(2,1fr); }
}
@media (max-width: 480px) {
  .stats-nums { grid-template-columns: repeat(2,1fr); gap: .35rem; margin-bottom: .35rem; }
  .stat-num   { padding: .45rem .6rem; }
  .stat-num i { width: 24px; height: 24px; font-size: .68rem; }
  .stat-num-val { font-size: 1.05rem; }
}

/* ── Category collapsed pill ── */
.cat-selected-pill {
  display: inline-flex; align-items: center; gap: .45rem;
  padding: .4rem .9rem; border: var(--bs-border-width) solid var(--bs-primary); border-radius: 999px;
  background: rgba(var(--bs-primary-rgb),.08); color: var(--bs-primary); cursor: pointer;
  font-family: 'DM Sans', sans-serif; font-size: .875rem; font-weight: 500;
  transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t); white-space: nowrap; margin-bottom: .75rem;
}
.cat-selected-pill:hover { background: rgba(var(--bs-primary-rgb),.14); }
.cat-selected-pill .fa-chevron-down { font-size: .55rem; opacity: .5; }
.btn-add-cat {
  background: none; border: var(--bs-border-width) dashed var(--bs-border-color); border-radius: 999px;
  padding: .28rem .75rem; font-size: .8rem; color: var(--bs-secondary-color); cursor: pointer;
  font-family: 'DM Sans', sans-serif; display: inline-flex; align-items: center; gap: .35rem;
  transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t);
}
.btn-add-cat:hover { border-color: var(--bs-primary); color: var(--bs-primary); }

/* ── Events grid ── */
.order-row {
  display: flex; align-items: flex-start; gap: .75rem; padding: .7rem 0;
  border-bottom: 1px solid var(--bs-border-color); font-size: .84rem;
}
.order-row:last-child { border-bottom: none; }
.order-event-name { font-weight: 600; color: var(--bs-emphasis-color); font-size: .82rem; }
.order-meta { font-size: .75rem; color: var(--bs-secondary-color); margin-top: .1rem; }
.events-section-title {
  font-weight: 700; font-size: .7rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--bs-secondary-color); display: flex; align-items: center; gap: .5rem; margin-bottom: 1rem;
}
/* "Who is this party for?" chip on the all-parties view: grey for a client, accent for Myself. */
.party-owner-chip { display: inline-flex; align-items: center; gap: .3rem; margin-bottom: .4rem; padding: .1rem .5rem; border-radius: 999px; font-size: .68rem; font-weight: 600; line-height: 1.5; letter-spacing: .01em; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; background: var(--bs-secondary-bg); color: var(--bs-secondary-color); }
.party-owner-chip i { font-size: .62rem; opacity: .85; }
.party-owner-chip--self { background: rgba(var(--bs-primary-rgb),.12); color: var(--bs-primary); }

/* ─────────────────────────────────────────────────────────────────────────
   "My Picture Parties" (all-parties) redesigned view — scoped to .mpp-mode so
   an individual client's page keeps its original two-card layout untouched.
   ───────────────────────────────────────────────────────────────────────── */
.mpp-header { display: none; }
.mpp-mode .mpp-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1.15rem; flex-wrap: wrap; }
.mpp-mode .mpp-title { font-size: 1.45rem; font-weight: 700; margin: 0; line-height: 1.15; color: var(--bs-emphasis-color); display: flex; align-items: center; gap: .55rem; }
.mpp-mode .mpp-title i { color: var(--bs-primary); font-size: 1.1rem; }
.mpp-mode .mpp-sub { margin: .3rem 0 0; font-size: .82rem; color: var(--bs-secondary-color); }
/* Owner filter — a segmented control with live counts, sitting above the list. Reads as an
   obvious control (connected buttons, filled active segment) unlike the old passive stat tiles. */
.mpp-owner-seg { display: none; }
.mpp-mode .mpp-owner-seg { display: inline-flex; flex-shrink: 0; margin: 0; border: 1px solid var(--bs-border-color); border-radius: var(--bs-border-radius); overflow: hidden; }
.mpp-seg { -webkit-appearance: none; appearance: none; font: inherit; cursor: pointer; border: 0; border-right: 1px solid var(--bs-border-color); background: var(--bs-tertiary-bg); color: var(--bs-secondary-color); padding: .3rem .7rem; font-size: .8rem; font-weight: 600; display: inline-flex; align-items: center; gap: .4rem; transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t); }
.mpp-seg:last-child { border-right: 0; }
.mpp-seg:hover { background: var(--bs-secondary-bg); color: var(--bs-emphasis-color); }
.mpp-seg.active { background: var(--bs-primary); color: var(--pp-on-primary, #fff); }
.mpp-seg-n { font-size: .72rem; font-weight: 700; font-variant-numeric: tabular-nums; background: rgba(128,128,128,.22); padding: .05rem .42rem; border-radius: 999px; }
.mpp-seg.active .mpp-seg-n { background: rgba(255,255,255,.28); }

/* The add-a-party form now lives in #addPartyModal (opened by the header "Add party" button on
   the all-parties page, and the Client-Info "Add Party" button on a client page). The inline
   card is gone, so hide the now-Client-Info-only row on the all-parties page. */
.mpp-mode .client-detail-row { display: none !important; }
.mpp-mode .mpp-add-btn { flex-shrink: 0; }
/* "or" divider inside the add-party modal (between the link field and the Scan button). */
.add-party-divider { display: flex; align-items: center; text-align: center; color: var(--bs-secondary-color); font-size: .78rem; margin: .8rem 0; }
.add-party-divider::before, .add-party-divider::after { content: ""; flex: 1; border-top: 1px solid var(--bs-border-color); }
.add-party-divider span { padding: 0 .7rem; }

/* The "Picture Parties  ·  N total" section label is now redundant with the header
   (override Bootstrap's .d-flex display:flex !important). */
.mpp-mode #eventsSection > .d-flex:first-child { display: none !important; }

/* The owner tag ("Myself" / client name) rides the top-left of the cover as a photo
   overlay chip instead of consuming a line in the body — reads as a tag, frees the body. */
.mpp-mode .party-owner-chip {
  position: absolute; top: .5rem; left: .5rem; z-index: 2; margin: 0;
  background: rgba(0,0,0,.6); color: #fff; box-shadow: 0 1px 5px rgba(0,0,0,.3);
  -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
}
.mpp-mode .party-owner-chip i { opacity: .9; }
.mpp-mode .party-owner-chip--self { background: rgba(var(--bs-primary-rgb),.92); color: var(--pp-on-primary, #fff); }

/* ═══════════════ My Picture Parties — style polish pass ═══════════════
   Cohesive rounding, calmer add-card, refined list rows + hover, empty state. */

/* Header — the icon becomes a soft accent chip; a touch more presence + room below. */
.mpp-mode .mpp-header { margin-bottom: 1.5rem; }
.mpp-mode .mpp-title { font-size: 1.5rem; letter-spacing: -.01em; gap: .6rem; }
.mpp-mode .mpp-title i { background: rgba(var(--bs-primary-rgb), .12); color: var(--bs-primary); padding: .42rem; border-radius: 10px; font-size: 1rem; line-height: 1; }

/* Add-party card — calmer and rounder so the list stays the star. */
.mpp-mode .client-detail-row { margin-bottom: 1.1rem !important; }
.mpp-mode .client-detail-row .card { box-shadow: none; }
.mpp-mode .client-detail-row .card-header { padding: .85rem 1.1rem 0; }
.mpp-mode .client-detail-row .card-body { padding: .55rem 1.1rem 1.05rem; }
#addPartyModal #addEventDesc { font-size: .82rem; }
.add-party-btn { font-weight: 600; }

/* Controls row — match the rounded language, give the search room to breathe above the
   filters, and separate the whole toolbar from the list below. */
#eventsControls { row-gap: .6rem; column-gap: .6rem; margin-bottom: 1rem !important; }
/* The search/date filter are -sm variants (Bootstrap gives them the sm radius
   tier), but the adjacent owner segment + Calendar button land on the base
   tier via the injected theme sheet — pin the whole toolbar to base so one
   row shares one corner radius. */
#eventsControls .form-control, #eventsControls .form-select { border-radius: var(--bs-border-radius); }
.mpp-mode #eventsSearch { margin-bottom: .2rem; }

.add-party-hint { margin: .45rem 0 0; font-size: .74rem; color: var(--bs-secondary-color); text-align: center; }

/* Empty state — centered, with an icon, instead of a lone line of text. (JS toggles
   inline display:block/none; these rules only style it once shown.) */
.mpp-mode #eventsNoResults { text-align: center; color: var(--bs-secondary-color); padding: 2.5rem 1rem; font-size: .9rem; }
.mpp-mode #eventsNoResults i { font-size: 1.6rem; opacity: .4; margin-bottom: .5rem; }

/* Party meta as clean stat items — date reads as primary, orders light up when there are any,
   and the "attendees" stat is simply omitted when the count is unknown (no more "— attendees"). */
.ec-stat { display: inline-flex; align-items: center; gap: .35rem; font-size: .78rem; color: var(--bs-secondary-color); white-space: nowrap; }
.ec-stat i { font-size: .72rem; opacity: .7; }
.ec-stat--date { color: var(--bs-emphasis-color); font-weight: 500; }
.ec-stat--has { color: var(--bs-primary); font-weight: 600; }

.et-date { white-space: nowrap; color: var(--bs-secondary-color); }
.et-actions-row { display: flex; align-items: center; gap: .3rem; justify-content: flex-end; }

/* ── Pagination ── */
.events-pagination { display: flex; align-items: center; justify-content: center; gap: .85rem; }
.events-page-info { font-size: .82rem; color: var(--bs-secondary-color); font-variant-numeric: tabular-nums; }

/* ── Parties data table (standard component, matches the affiliates/clients tables) ──────
   Scroll container keeps the header pinned while you scroll the rows; the pager sits below,
   pinned to the viewport bottom. */
/* The parties list already lives in a scrolling column (.col-lg-8.overflow-auto), so the table
   must NOT create its own scroll box — that nested scroller was the "little cell". Let it flow
   full-length; the column scrolls and the sticky <thead> pins to the column top. */
.pp-table-wrap { overflow: visible; }
.pp-data-table { margin-bottom: 0; }
/* Sticky pager: a plain full-width bar pinned to the bottom as you scroll (no boxed card). */
.pp-sticky-pager { position: sticky; bottom: 0; z-index: 3; padding: .6rem 0; margin-top: 0;
  background: var(--bs-body-bg); border-top: 1px solid var(--bs-border-color); }

/* Parties-table cells */
.ce-th-thumb, .ce-td-thumb { width: 56px; }
.ce-td-thumb { padding-right: .25rem !important; }
.ce-thumb-img { width: 46px; height: 34px; object-fit: cover; border-radius: var(--bs-border-radius-sm); display: block; }
.ce-thumb-failed { display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, rgba(var(--bs-primary-rgb),.10), var(--bs-tertiary-bg) 72%); color: rgba(var(--bs-primary-rgb),.5); font-size: .8rem; }
/* The "Myself" chip in the For column is a plain inline chip here (override the card-overlay
   rule that absolutely-positions .party-owner-chip). */
.pp-data-table .party-owner-chip { position: static; margin: 0; background: rgba(var(--bs-primary-rgb),.12); color: var(--bs-primary); box-shadow: none; -webkit-backdrop-filter: none; backdrop-filter: none; }
.ce-client-link { color: var(--bs-primary); cursor: pointer; display: inline-flex; align-items: center; gap: .35rem; }
.ce-client-link:hover { text-decoration: underline; }
.ce-client-link i { font-size: .7rem; opacity: .75; }
.pp-data-table .aff-name-primary { font-weight: 600; }
.pp-data-table .aff-name-primary:hover { color: var(--bs-primary); }
/* Compact row actions — all borderless icons in the muted secondary color, so the Print Studio
   button matches its siblings (open / re-scrape / delete). */
.pp-data-table .et-actions-row { display: flex; align-items: center; gap: .15rem; justify-content: flex-end; flex-wrap: nowrap; }
.pp-data-table .btn-print-collateral { flex: 0 0 auto; min-width: 0; background: transparent !important; color: var(--bs-secondary-color) !important; border: none !important; box-shadow: none !important; border-radius: var(--bs-border-radius-sm); padding: .3rem .4rem; line-height: 1; }
.pp-data-table .btn-print-collateral:hover { background: var(--bs-secondary-bg) !important; }
.pp-data-table .btn-upload-banner { border: none; background: transparent; color: var(--bs-secondary-color); padding: .3rem .4rem; border-radius: var(--bs-border-radius-sm); line-height: 1; }
.pp-data-table .btn-upload-banner:hover { background: var(--bs-secondary-bg); }

/* Collapsible context calendar (collapsed by default; toggled by an inline button in the
   parties controls row). */
#ctxSplit.cal-collapsed > .col-lg-8 { flex: 0 0 100%; max-width: 100%; }
@media (min-width: 992px) { #ctxSplit.cal-collapsed #ctxCalSidebar { display: none !important; } }
/* (min-width:100% for frozen tables now lives on the shared .affmgr-table.pp-cols-frozen
   rule below, so every manager table fills the pane — not just #eventsTable.) */
.pp-data-table td, .pp-data-table thead th { padding-left: .6rem; padding-right: .6rem; }
/* On the all-parties page, break the table (+ its sticky pager) out of #clientPanel's padding
   so it spans the full content width edge-to-edge, like the affiliates/clients tables. */
.mpp-mode #eventsTableWrap, .mpp-mode #eventsPagination { margin-left: -1rem; margin-right: -1rem; }
@media (min-width: 992px) { .mpp-mode #eventsTableWrap, .mpp-mode #eventsPagination { margin-left: -1.5rem; margin-right: -1.5rem; } }
.ce-thumb-img { width: 42px; height: 30px; }
.btn-print-collateral {
  flex: 1; background: var(--bs-primary); color: var(--pp-on-primary, #fff); border: none; border-radius: var(--bs-border-radius);
  padding: .42rem .65rem; font-size: .76rem; font-family: 'DM Sans', sans-serif;
  font-weight: 600; cursor: pointer; display: flex; align-items: center;
  justify-content: center; gap: .3rem; transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t);
}
.btn-print-collateral:hover { background: color-mix(in srgb, var(--bs-primary) 85%, black); color: #fff; }
.btn-event-orders {
  background: none; border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius);
  padding: .38rem .55rem; font-size: .72rem; cursor: pointer; color: var(--bs-secondary-color);
  font-family: 'DM Sans', sans-serif; transition: all .2s; white-space: nowrap;
  display: flex; align-items: center; gap: .25rem;
}
.btn-event-orders:hover { border-color: var(--bs-primary); color: var(--bs-primary); }

/* ── Bootstrap Modal overrides ── */
.pp-modal { padding: 2rem; border-radius: var(--bs-border-radius-lg); border: none; box-shadow: 0 8px 32px rgba(0,0,0,.16); }
.pp-modal h5 { font-weight: 700; font-size: 1rem; color: var(--bs-emphasis-color); margin-bottom: 1.25rem; }

/* Bootstrap-structured catalog modals */
.pp-modal-bs { border-radius: var(--bs-border-radius-lg); border: none; box-shadow: 0 8px 32px rgba(0,0,0,.16); background: var(--bs-body-bg); }
.pp-modal-bs .modal-header { padding: 1.25rem 1.5rem 1rem; border-bottom: 1px solid var(--bs-border-color); }
.pp-modal-bs .modal-title  { font-weight: 700; font-size: 1rem; color: var(--bs-emphasis-color); }
.pp-modal-bs .modal-body   { padding: 1.25rem 1.5rem; }
.pp-modal-bs .modal-footer { padding: .85rem 1.5rem 1.25rem; border-top: 1px solid var(--bs-border-color); gap: .6rem; }

/* Step-number badge for the workflow sections inside Order Details
   (Step 1 Cards, Step 2/3 Shipping, etc.). Small circular numeral that
   visually anchors each section header. */
.pp-step-num {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 50%;
  background: var(--bs-primary); color: var(--pp-on-primary, #fff);
  font-size: .78rem; font-weight: 700;
  flex-shrink: 0;
}

/* ── Print job live-progress modal ──────────────────────────────────
   Opens after Print Cards. Header (icon + title + close), striped
   progress bar that goes solid green on completion, then a single
   status line and any printer-state warning. */
.pp-print-job-modal { border-radius: var(--bs-border-radius-lg); border: none; box-shadow: 0 8px 32px rgba(0,0,0,.16); }
.pp-print-job-modal .modal-body   { padding: 1.25rem 1.5rem .75rem; }
.pp-print-job-modal .pp-pj-footer { padding: .65rem 1.25rem 1rem; border-top: 1px solid var(--bs-border-color); }
.pp-pj-hdr {
  display: grid; grid-template-columns: auto 1fr auto;
  align-items: center; gap: .85rem; margin-bottom: .85rem;
}
.pp-pj-icon {
  width: 38px; height: 38px; border-radius: 9px;
  background: rgba(var(--bs-primary-rgb),.1); color: var(--bs-primary);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 1rem;
}
.pp-pj-titles { min-width: 0; }
.pp-pj-title  { font-weight: 700; font-size: 1rem; color: var(--bs-emphasis-color); line-height: 1.2; }
.pp-pj-sub    { font-size: .78rem; color: var(--bs-secondary-color); margin-top: .15rem;
                white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* 4-stage indicator (Queued → Sending → Printing → Done). Each stage is
   a small dot + label; connectors between them fill in as the job
   progresses. State is driven by real signals (CUPS job state + printer
   hardware state via IPP), not a timer — no fake progress bar. */
.pp-pj-stages {
  display: flex; align-items: center; justify-content: space-between;
  gap: .25rem; margin: .25rem 0 .85rem;
}
.pp-pj-stage {
  display: flex; flex-direction: column; align-items: center;
  gap: .25rem; flex: 0 0 auto;
}
.pp-pj-stage-dot {
  width: 28px; height: 28px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: .72rem;
  border: var(--bs-border-width) solid var(--bs-border-color);
  background: var(--bs-body-bg);
  color: var(--bs-secondary-color);
  transition: background .2s, border-color .2s, color .2s;
}
.pp-pj-stage-label {
  font-size: .68rem; font-weight: 600;
  color: var(--bs-secondary-color);
}
.pp-pj-stage-conn {
  flex: 1 1 0; height: 2px;
  background: var(--bs-border-color);
  transition: background .25s;
}
/* Pending: unfilled, muted. */
.pp-pj-stage--pending .pp-pj-stage-dot   { background: var(--bs-body-bg); color: var(--bs-secondary-color); }
.pp-pj-stage--pending .pp-pj-stage-label { color: var(--bs-secondary-color); }
/* Active: filled with primary, label emphasized + subtle pulse. */
.pp-pj-stage--active .pp-pj-stage-dot {
  background: var(--bs-primary); color: var(--pp-on-primary, #fff);
  border-color: var(--bs-primary);
  animation: pp-pj-stage-pulse 1.4s ease-in-out infinite;
}
.pp-pj-stage--active .pp-pj-stage-label { color: var(--bs-emphasis-color); }
/* Done: filled with success green, no pulse. */
.pp-pj-stage--done .pp-pj-stage-dot {
  background: var(--bs-success); color: #fff; border-color: var(--bs-success);
}
.pp-pj-stage--done .pp-pj-stage-label { color: var(--bs-secondary-color); }
/* Stopped: muted, no animation. */
.pp-pj-stage--stopped .pp-pj-stage-dot {
  background: var(--bs-secondary-bg); color: var(--bs-secondary-color);
  border-color: var(--bs-border-color);
}
/* Connector fill: lit between consecutive done stages (and between the
   last done stage and the active stage). */
.pp-pj-stage--done + .pp-pj-stage-conn,
.pp-pj-stage--done + .pp-pj-stage-conn + .pp-pj-stage--active ~ .pp-pj-stage-conn:not(:has(+ .pp-pj-stage--pending)) {
  /* fallback handled below — using direct + sibling selectors */
}
.pp-pj-stages .pp-pj-stage--done + .pp-pj-stage-conn { background: var(--bs-success); }
@keyframes pp-pj-stage-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(var(--bs-primary-rgb), .35); }
  50%      { box-shadow: 0 0 0 6px rgba(var(--bs-primary-rgb), 0); }
}
/* Real per-page progress (driven by IPP job-impressions-completed). The
   bar fills based on actual physically-printed sheets, not a time guess. */
.pp-pj-pages-wrap { margin: .25rem 0 .65rem; }
.pp-pj-pages {
  font-size: .78rem; color: var(--bs-emphasis-color); font-weight: 600;
  margin-bottom: .25rem;
}
.pp-pj-bar-track {
  height: 8px; border-radius: 4px; overflow: hidden;
  background: var(--bs-tertiary-bg);
}
.pp-pj-bar-fill {
  height: 100%; width: 0%;
  background: var(--bs-primary);
  transition: width .5s ease;
}
.pp-pj-state {
  font-size: .9rem; font-weight: 600; color: var(--bs-emphasis-color);
  margin-bottom: .25rem;
}
.pp-pj-detail {
  font-size: .8rem; color: var(--bs-secondary-color); line-height: 1.4;
  min-height: 1.2em;
}
.pp-pj-elapsed {
  font-size: .72rem; color: var(--bs-secondary-color);
  margin-top: .45rem; font-variant-numeric: tabular-nums;
}
.pp-pj-meta {
  font-size: .68rem; color: var(--bs-secondary-color);
  margin-top: .55rem; font-family: monospace;
}
.pp-pj-meta code { background: var(--bs-tertiary-bg); padding: .05rem .25rem; border-radius: 3px; font-size: .9em; }
.pp-pj-warn { color: var(--bs-warning-text-emphasis, #b45309); font-weight: 600; }
.pp-pj-warn i { margin-right: .35rem; }

/* ── Order fulfillment pipeline ──────────────────────────────────────
   A horizontal journey stepper at the top of Order Details: Ordered →
   PDF Ready → In Production → Label → Shipped → Delivered. Mirrors the
   print-job stage indicator above; driven purely by fulfillment_status. */
.op-card .card-body { padding: 1rem 1.15rem .95rem; }
.op-track {
  display: flex; align-items: flex-start; justify-content: space-between; gap: .25rem;
}
.op-track--cancelled { opacity: .5; }
.op-stage {
  display: flex; flex-direction: column; align-items: center; gap: .35rem;
  flex: 0 0 auto; width: 76px; text-align: center;
}
.op-stage-dot {
  width: 32px; height: 32px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: .8rem;
  border: var(--bs-border-width) solid var(--bs-border-color);
  background: var(--bs-body-bg); color: var(--bs-secondary-color);
  transition: background .2s, border-color .2s, color .2s, box-shadow .2s;
}
.op-stage-label {
  font-size: .7rem; font-weight: 600; line-height: 1.15;
  color: var(--bs-secondary-color);
}
/* Connector fills green once the stage before it is complete (matches the
   print-job stepper's + sibling rule). Aligned to the dot centre. */
.op-stage-conn {
  flex: 1 1 0; min-width: 10px; height: 2px; margin-top: 15px;
  background: var(--bs-border-color); transition: background .25s;
}
.op-track .op-stage--done + .op-stage-conn { background: var(--bs-success); }
.op-stage--done .op-stage-dot   { background: var(--bs-success); color: #fff; border-color: var(--bs-success); }
.op-stage--done .op-stage-label { color: var(--bs-body-color); }
.op-stage--active .op-stage-dot {
  background: var(--bs-primary); color: var(--pp-on-primary, #fff); border-color: var(--bs-primary);
  box-shadow: 0 0 0 4px rgba(var(--bs-primary-rgb), .16);
}
.op-stage--active .op-stage-label { color: var(--bs-emphasis-color); font-weight: 700; }
.op-stage--printing .op-stage-dot { animation: pp-pj-stage-pulse 1.4s ease-in-out infinite; }
/* "Printing now" live chip — a blinking dot + link back to the live modal. */
.op-live {
  margin-top: .9rem; display: inline-flex; align-items: center; gap: .5rem;
  border: none; border-radius: 999px; cursor: pointer;
  background: rgba(var(--bs-primary-rgb), .1); color: var(--bs-primary);
  font-size: .78rem; font-weight: 700; padding: .38rem .8rem;
}
.op-live:hover { background: rgba(var(--bs-primary-rgb), .18); }
.op-live-dot {
  width: 8px; height: 8px; border-radius: 50%; background: var(--bs-primary);
  animation: op-live-blink 1s steps(2, start) infinite;
}
@keyframes op-live-blink { 50% { opacity: .2; } }
.op-cancel { margin-top: .7rem; color: var(--bs-danger); font-size: .82rem; font-weight: 600; }
@media (max-width: 640px) {
  .op-track { justify-content: flex-start; overflow-x: auto; padding-bottom: .35rem; gap: .1rem; }
  .op-stage { width: 66px; }
  .op-stage-conn { min-width: 16px; }
}

/* ── Avatars ── */
.pp-avatar {
  border-radius: 50%; object-fit: cover; display: inline-block;
  vertical-align: middle; flex-shrink: 0;
}
.pp-avatar-initials {
  border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
  font-weight: 700; color: rgba(0,0,0,.38); font-family: 'DM Sans', sans-serif;
  letter-spacing: -.01em; user-select: none; flex-shrink: 0;
}
.pp-avatar-wrap {
  position: relative; display: inline-flex; cursor: pointer; flex-shrink: 0;
}
.pp-avatar-wrap:hover .pp-avatar-overlay { opacity: 1; }
.pp-avatar-overlay {
  position: absolute; inset: 0; border-radius: 50%;
  background: rgba(0,0,0,.38); display: flex; align-items: center; justify-content: center;
  opacity: 0; transition: opacity .15s; color: #fff; font-size: .7rem; pointer-events: none;
}

/* ── Bootstrap badge overrides — subtle tinted style ── */
.badge { font-family: 'DM Sans', sans-serif; font-weight: 700; }
.badge.text-bg-secondary { background-color: rgba(var(--bs-secondary-rgb),.15) !important; color: var(--bs-secondary-color) !important; }
.badge.text-bg-primary   { background-color: rgba(var(--bs-primary-rgb),.12)   !important; color: var(--bs-primary)          !important; }
.badge.text-bg-success   { background-color: rgba(var(--bs-success-rgb),.14)   !important; color: var(--bs-success-text-emphasis) !important; }
.badge.text-bg-warning   { background-color: rgba(var(--bs-warning-rgb),.14)   !important; color: var(--bs-warning-text-emphasis) !important; }
.badge.text-bg-danger    { background-color: rgba(var(--bs-danger-rgb),.14)    !important; color: var(--bs-danger-text-emphasis)  !important; }
.badge.text-bg-info      { background-color: rgba(var(--bs-info-rgb),.14)      !important; color: var(--bs-info-text-emphasis)    !important; }

/* ── Bootstrap alert color overrides — via Bootstrap's own vars ── */
.alert-danger  { --bs-alert-bg: #fef2f2; --bs-alert-border-color: #fecaca; --bs-alert-color: #dc2626; }
.alert-success { --bs-alert-bg: #f0fdf4; --bs-alert-border-color: #bbf7d0; --bs-alert-color: #15803d; }
[data-bs-theme="dark"] .alert-danger  { --bs-alert-bg: rgba(220,38,38,.12); --bs-alert-border-color: rgba(220,38,38,.3); --bs-alert-color: #f87171; }
[data-bs-theme="dark"] .alert-success { --bs-alert-bg: rgba(22,163,74,.12); --bs-alert-border-color: rgba(22,163,74,.3); --bs-alert-color: #4ade80; }

/* ── Category picker ── */
.cat-grid {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: .4rem; margin-bottom: .75rem;
}
.cat-btn {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .25rem; padding: .5rem .2rem; border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius);
  background: var(--bs-body-bg); cursor: pointer; transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t);
  font-family: 'DM Sans', sans-serif; font-size: .65rem; color: var(--bs-secondary-color);
  text-align: center; line-height: 1.2;
}
.cat-btn i { font-size: .95rem; color: var(--bs-secondary-color); transition: color .15s; }
.cat-btn:hover:not(.selected) { border-color: var(--bs-border-color); background: var(--bs-tertiary-bg); }
.cat-btn.selected { border-color: var(--bs-primary); background: rgba(var(--bs-primary-rgb),.08); color: var(--bs-primary); }
.cat-btn.selected i { color: var(--bs-primary); }
/* Category picker — dropdown variant (trigger + menu) */
.cat-picker { position: relative; }
.cat-picker-trigger {
  display: flex; align-items: center; gap: .5rem; width: 100%;
  padding: .5rem .75rem; border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius);
  background: var(--bs-body-bg); color: var(--bs-body-color); cursor: pointer;
  font-family: 'DM Sans', sans-serif; font-size: .86rem; text-align: left;
}
.cat-picker-trigger:hover { border-color: var(--bs-secondary-color); }
.cat-picker.open .cat-picker-trigger { border-color: var(--bs-primary); }
.cat-picker-trigger > span:not(.cat-picker-caret) { flex: 1 1 auto; }
.cat-picker-trigger > i:first-child { width: 1rem; text-align: center; color: var(--bs-secondary-color); }
.cat-picker-ph { color: var(--bs-secondary-color); }
.cat-picker-caret { margin-left: auto; font-size: .6rem; opacity: .5; transition: transform .15s; }
.cat-picker.open .cat-picker-caret { transform: rotate(180deg); }
.cat-picker-menu {
  position: absolute; top: calc(100% + 3px); left: 0; right: 0; z-index: 200; display: none;
  background: var(--bs-body-bg); border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius);
  box-shadow: 0 4px 20px rgba(0,0,0,.12); max-height: 280px; overflow-y: auto;
}
.cat-picker.open .cat-picker-menu { display: block; }

/* ── Bootstrap nav override for admin tabs ── */
.admin-tabs {
  position: sticky; top: 0; z-index: 1; flex-wrap: nowrap;
  background: var(--bs-tertiary-bg); border-bottom: 1px solid var(--bs-border-color);
  border-radius: 0; gap: 0;
}
.admin-tabs .nav-link {
  flex: 1; padding: .45rem .5rem; font-size: .72rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .05em;
  background: none; border: none; border-bottom: 2px solid transparent;
  border-radius: 0; cursor: pointer;
  color: var(--bs-secondary-color); font-family: 'DM Sans', sans-serif; transition: all .15s;
}
.admin-tabs .nav-link.active { color: var(--bs-primary); border-bottom-color: var(--bs-primary); background: none; }
.admin-tabs .nav-link:hover:not(.active) { color: var(--bs-emphasis-color); background: none; }

/* ── Address autocomplete dropdown ── */
#clientAddressDropdown, #profileAddressDropdown, #shipFormAddrDropdown {
  display: none; position: absolute; top: 100%; left: 0; right: 0; z-index: 9999;
  background: var(--bs-body-bg); border: var(--bs-border-width) solid rgba(var(--bs-primary-rgb),.25); border-top: none;
  border-radius: 0 0 var(--bs-border-radius) var(--bs-border-radius); box-shadow: 0 8px 24px rgba(0,0,0,.1);
  list-style: none; margin: 0; padding: .25rem 0; max-height: 200px; overflow-y: auto;
}
#clientAddressDropdown li, #profileAddressDropdown li, #shipFormAddrDropdown li {
  display: flex; align-items: center; gap: .5rem;
  padding: .5rem .75rem; cursor: pointer;
  font-family: 'DM Sans', sans-serif; font-size: .875rem;
  border-bottom: 1px solid var(--bs-border-color); color: var(--bs-body-color);
}
#clientAddressDropdown li:last-child, #profileAddressDropdown li:last-child, #shipFormAddrDropdown li:last-child { border-bottom: none; }
#clientAddressDropdown li:hover, #clientAddressDropdown li.pac-active,
#profileAddressDropdown li:hover, #profileAddressDropdown li.pac-active,
#shipFormAddrDropdown li:hover, #shipFormAddrDropdown li.pac-active { background: var(--bs-tertiary-bg); }
#clientAddressDropdown .pac-icon, #profileAddressDropdown .pac-icon, #shipFormAddrDropdown .pac-icon { color: var(--bs-primary); font-size: .75rem; flex-shrink: 0; }
#clientAddressDropdown .pac-secondary, #profileAddressDropdown .pac-secondary, #shipFormAddrDropdown .pac-secondary { color: var(--bs-secondary-color); font-size: .78rem; }

/* ── Shipping address modal ── */
.shipping-opt-card {
  border: var(--bs-border-width) solid var(--bs-border-color); border-radius: .5rem;
  padding: .7rem .9rem; cursor: pointer;
  transition: border-color .15s, background .15s;
}
.shipping-opt-card:hover { border-color: var(--bs-primary); background: rgba(var(--bs-primary-rgb), .03); }
.shipping-opt-card.selected { border-color: var(--bs-primary); background: rgba(var(--bs-primary-rgb), .06); }
.ship-radio {
  width: 1rem; height: 1rem; border-radius: 50%;
  border: 2px solid var(--bs-border-color);
  transition: border-color .15s, border-width .1s;
  flex-shrink: 0;
}
.shipping-opt-card.selected .ship-radio { border: 5px solid var(--bs-primary); }
.saved-addr-chip { text-align: left; white-space: normal; font-size: .8rem; }

/* ── Calendar widget ── */
/* Aether: ghost buttons — the three 1.5px-bordered squares crowded the small
   card header; hover/active still restore a primary border for affordance. */
.cal-nav-btn { background:none; border:var(--bs-border-width) solid transparent; border-radius:var(--bs-border-radius); width:28px; height:28px; cursor:pointer; color:var(--bs-secondary-color); display:flex; align-items:center; justify-content:center; font-size:.68rem; transition:all .15s; flex-shrink:0; }
.cal-nav-btn:hover { border-color:var(--bs-primary); color:var(--bs-primary); background:rgba(var(--bs-primary-rgb),.06); }
.cal-nav-btn.active { background:var(--bs-primary); border-color:var(--bs-primary); color:var(--pp-on-primary, #fff); }
.cal-today-btn { background:none; border:var(--bs-border-width) solid transparent; border-radius:var(--bs-border-radius); padding:0 .55rem; height:28px; cursor:pointer; color:var(--bs-secondary-color); font-size:.68rem; font-weight:600; font-family:inherit; display:flex; align-items:center; transition:all .15s; white-space:nowrap; }
.cal-today-btn:hover { border-color:var(--bs-primary); color:var(--bs-primary); background:rgba(var(--bs-primary-rgb),.06); }
.cal-grid { display:grid; grid-template-columns:repeat(7,1fr); gap:2px; }
.cal-dow { font-size:.57rem; font-weight:700; color:var(--bs-secondary-color); text-align:center; padding:.45rem 0 .3rem; text-transform:uppercase; letter-spacing:.05em; }
.cal-day { display:flex; flex-direction:column; align-items:center; justify-content:flex-start; padding:.28rem .05rem .3rem; border-radius:8px; min-height:44px; transition:background .12s; }
.cal-day.cal-has-events, .cal-day.cal-has-ann { cursor:pointer; }
.cal-day-num { font-size:.78rem; color:var(--bs-body-color); width:26px; height:26px; display:flex; align-items:center; justify-content:center; border-radius:50%; line-height:1; transition:background .12s, color .12s; font-weight:400; }
.cal-day.cal-weekend .cal-day-num { color:var(--bs-secondary-color); }
.cal-today .cal-day-num { background:var(--bs-primary); color:var(--pp-on-primary, #fff) !important; font-weight:700; }
.cal-has-events.status-upcoming:not(.cal-today) .cal-day-num { color:#15803d; font-weight:600; }
.cal-has-events.status-past:not(.cal-today)     .cal-day-num { color:var(--bs-secondary-color); }
.cal-day.cal-has-events:hover { background:rgba(var(--bs-primary-rgb),.08); }
.cal-day.cal-has-events.status-upcoming:not(.cal-today):hover { background:rgba(21,128,61,.08); }
.cal-day.cal-has-events.status-past:hover { background:var(--bs-tertiary-bg); }
.cal-day.cal-has-ann:not(.cal-has-events):hover { background:rgba(217,119,6,.1); }
.cal-day.cal-today.cal-has-events:hover .cal-day-num { background:var(--bs-primary); }
.cal-indicators { display:flex; gap:3px; align-items:center; margin-top:3px; min-height:6px; }
.cal-pip { width:18px; height:3px; border-radius:2px; flex-shrink:0; }
.cal-pip.pip-past     { background:#64748b; }   /* slate-500 — 5.3:1 UI on #fff, 4.3:1 on #212529 */
.cal-pip.pip-today    { background:var(--bs-primary); }
.cal-pip.pip-upcoming { background:#15803d; }
.cal-pip.pip-ann      { background:var(--gold,#d97706); }
.cal-ev-extra { font-size:.5rem; font-weight:700; color:var(--bs-secondary-color); line-height:1; }
.cal-ann-only .cal-indicators { opacity:.7; }
.cal-btn-sidebar { background:none; border:var(--bs-border-width) solid transparent; border-radius:var(--bs-border-radius); width:28px; height:28px; cursor:pointer; color:var(--bs-secondary-color); display:flex; align-items:center; justify-content:center; font-size:.72rem; transition:all .15s; flex-shrink:0; }
.cal-btn-sidebar:hover { border-color:var(--bs-primary); color:var(--bs-primary); background:rgba(var(--bs-primary-rgb),.08); }
/* ── Event date input ── */
.event-date-input { border: none; background: none; font-family: 'DM Sans', sans-serif; font-size: .76rem; color: var(--bs-body-color); cursor: pointer; padding: 0; max-width: 130px; outline: none; }
.event-date-input::-webkit-calendar-picker-indicator { opacity: 0.45; cursor: pointer; margin-left: 2px; }
.event-date-input:hover::-webkit-calendar-picker-indicator { opacity: 0.8; }
/* ── Events table view ── */
.event-table-row.status-upcoming > td:nth-child(2) { border-left: 3px solid #15803d; padding-left: .5rem; }
.event-table-row.status-today > td:nth-child(2)    { border-left: 3px solid var(--bs-primary); padding-left: .5rem; }
.event-table-row.status-past > td:nth-child(2)     { border-left: 3px solid var(--bs-border-color); padding-left: .5rem; }

/* ── Settings offcanvas ── */
.settings-section {
  margin-bottom: 1rem; padding-bottom: 1rem; border-bottom: 1px solid var(--bs-border-color);
}
.settings-section:last-of-type { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }

.settings-section-hdr {
  font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
  color: var(--bs-secondary-color); margin-bottom: .75rem;
  display: flex; align-items: center; gap: .4rem;
}
.settings-section-hdr i { font-size: .78rem; }

.settings-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: .4rem 0; gap: 1rem;
}
.settings-label { font-size: .875rem; font-weight: 600; color: var(--bs-body-color); }
.settings-desc  { font-size: .775rem; color: var(--bs-secondary-color); margin-top: .1rem; line-height: 1.4; }

/* ── Sidebar panels ── */
#normalSidebarContent { display: flex; flex-direction: column; flex: 1; min-height: 0; overflow-y: auto; }
#adminSidebarSection { flex-direction: column; }
.sidebar-section-label {
  padding: .6rem 1.25rem .3rem;
  font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
  color: var(--bs-secondary-color);
}
/* Collapsible drawer that wraps the admin dashboard's quick-stats,
   charts, and top-affiliates list as a single visual card. */
.admin-dash-drawer {
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bs-border-radius-lg);
  background: var(--bs-body-bg);
  margin-bottom: .75rem;
  overflow: hidden;
}
.admin-dash-drawer-hdr {
  width: 100%; display: flex; align-items: center; gap: .5rem;
  padding: .6rem .9rem;
  background: transparent; border: 0;
  text-align: left; cursor: pointer;
  font-size: .9rem; font-weight: 600; color: var(--bs-emphasis-color);
}
.admin-dash-drawer-hdr:hover { background: var(--bs-tertiary-bg); }
.admin-dash-drawer-hdr[aria-expanded="true"] { border-bottom: 1px solid var(--bs-border-color); }
.admin-dash-drawer-body { padding: .75rem; }
.admin-dash-chevron { font-size: .75rem; transition: transform .15s ease; color: var(--bs-secondary-color); }
.admin-dash-drawer-hdr[aria-expanded="true"] .admin-dash-chevron { transform: rotate(90deg); }

/* Admin-only "Logged in as <affiliate>" banner. Sits above the topbar
   while an admin is impersonating another user. */
.impersonation-banner {
  display: flex; align-items: center; gap: .75rem;
  padding: .4rem .9rem;
  background: #f59e0b; color: #1f2937;
  font-size: .85rem; font-weight: 600;
  flex-shrink: 0;
  box-shadow: 0 1px 2px rgba(0,0,0,.08);
}
.impersonation-banner-exit {
  margin-left: auto;
  background: rgba(0,0,0,.08); color: inherit;
  border: 0; border-radius: var(--bs-border-radius-sm);
  padding: .25rem .65rem; font-size: .8rem; font-weight: 600; cursor: pointer;
}
.impersonation-banner-exit:hover { background: rgba(0,0,0,.18); }

/* ── Connection-blocked overlay (ad-blocker self-help) ── */
#connBlockedScreen {
  position: fixed; inset: 0; z-index: 12000;
  background: var(--bs-body-bg);
  display: flex; align-items: center; justify-content: center; padding: 1.5rem;
}
.conn-blocked-card {
  max-width: 460px; width: 100%; text-align: center;
  background: var(--bs-body-bg); color: var(--bs-body-color);
  border: 1px solid var(--bs-border-color); border-radius: var(--bs-border-radius-lg);
  padding: 2rem 1.75rem; box-shadow: 0 8px 32px rgba(0,0,0,.12);
}
.conn-blocked-icon { font-size: 2.4rem; color: var(--bs-primary); margin-bottom: .75rem; }
.conn-blocked-card h2 { font-size: 1.25rem; font-weight: 700; margin: 0 0 .75rem; }
.conn-blocked-card p { font-size: .92rem; color: var(--bs-secondary-color); margin: 0 0 .75rem; }
.conn-blocked-steps { font-weight: 600; color: var(--bs-body-color) !important; margin-bottom: .35rem !important; }
.conn-blocked-card ul { text-align: left; font-size: .9rem; margin: 0 0 1rem; padding-left: 1.2rem; }
.conn-blocked-card li { margin: .4rem 0; color: var(--bs-body-color); }
.conn-blocked-foot { font-size: .82rem; margin-top: 1rem !important; }

/* ── TOS acceptance banner ── */
.tos-banner {
  display: flex; align-items: center; gap: .65rem; flex-wrap: wrap;
  padding: .55rem 1rem;
  background: var(--bs-primary); color: var(--pp-on-primary, #fff);
  font-size: .88rem; font-weight: 500; flex-shrink: 0;
  box-shadow: 0 1px 3px rgba(0,0,0,.12);
}
.tos-banner > i { font-size: 1rem; flex-shrink: 0; }
.tos-banner-text { flex: 1 1 auto; min-width: 12rem; }
.tos-banner-review {
  color: #fff; text-decoration: underline; font-weight: 600; white-space: nowrap;
}
.tos-banner-review:hover { color: #fff; opacity: .85; }
.tos-banner-accept {
  background: #fff; color: var(--bs-primary); border: 0; border-radius: var(--bs-border-radius);
  padding: .35rem .9rem; font-size: .85rem; font-weight: 700; cursor: pointer;
  white-space: nowrap; flex-shrink: 0;
}
.tos-banner-accept:hover { opacity: .9; }
.tos-banner-accept:disabled { opacity: .7; cursor: default; }

.admin-search-bar {
  flex-shrink: 0; padding: .6rem 1rem; border-bottom: 1px solid var(--bs-border-color);
  display: flex; align-items: center; gap: .4rem; position: relative; background: var(--bs-tertiary-bg);
}
.admin-search-bar input {
  flex: 1; min-width: 0; border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius);
  padding: .5rem .75rem; font-size: .88rem; outline: none;
  transition: border-color .2s; font-family: 'DM Sans', sans-serif;
  background: var(--bs-body-bg); color: var(--bs-body-color);
}
.admin-search-bar input:focus { border-color: var(--bs-primary); }
#adminCatSelect { background: var(--bs-body-bg); color: var(--bs-body-color); border-color: var(--bs-border-color); }
#adminCatSelect:focus { outline: none; border-color: var(--bs-primary); }
#adminCatSelect.active { border-color: var(--bs-primary); color: var(--bs-primary); }
.admin-list-scroll { flex: 1; min-height: 0; overflow-y: auto; }
#adminSortBar { display:flex; justify-content:space-between; align-items:center; padding:.25rem .9rem; border-bottom:1px solid var(--bs-border-color); flex-shrink:0; position:relative; }
#adminSortDropdown { position:absolute; top:calc(100% + 2px); right:.5rem; z-index:200; background:var(--bs-body-bg); border-radius:var(--bs-border-radius); box-shadow:0 4px 20px rgba(0,0,0,.12); min-width:160px; display:none; overflow:hidden; }
.affiliate-item {
  padding: .65rem 1.25rem; cursor: pointer;
  border-left: 3px solid transparent; transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t);
}
.affiliate-item:hover { background: var(--bs-tertiary-bg); }
.affiliate-item.active { background: rgba(220,38,38,.1); border-left-color: #dc2626; }
.affiliate-item-name { font-weight: 600; font-size: .9rem; color: var(--bs-emphasis-color); }
.affiliate-item-sub  { font-size: .78rem; color: var(--bs-secondary-color); margin-top: 2px; }

/* ── Announcements ── */
#announcementsSection { max-width: none; margin: 0; }
/* pending image thumbnails in composer */
.ann-pending-media { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .55rem; }
.ann-pending-thumb { position: relative; width: 64px; height: 64px; border-radius: var(--bs-border-radius-sm); overflow: hidden; flex-shrink: 0; }
.ann-pending-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ann-pending-thumb .rm-btn {
  position: absolute; top: 2px; right: 2px; background: rgba(0,0,0,.55); border: none;
  border-radius: 50%; width: 24px; height: 24px; color: #fff; cursor: pointer;
  font-size: .7rem; display: flex; align-items: center; justify-content: center;
}
/* poll composer */
.ann-poll-composer { margin-top: .65rem; border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius); padding: .6rem .75rem; }
.ann-poll-composer-q { width: 100%; border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius-sm); padding: .35rem .6rem; font-family: 'DM Sans',sans-serif; font-size: .85rem; background: var(--bs-body-bg); color: var(--bs-body-color); outline: none; margin-bottom: .45rem; }
.ann-poll-composer-q:focus { border-color: var(--bs-primary); }
.ann-poll-opt-row { display: flex; gap: .4rem; margin-bottom: .35rem; }
.ann-poll-opt-input { flex: 1; border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius-sm); padding: .3rem .55rem; font-family: 'DM Sans',sans-serif; font-size: .83rem; background: var(--bs-body-bg); color: var(--bs-body-color); outline: none; }
.ann-poll-opt-input:focus { border-color: var(--bs-primary); }
.btn-rm-opt { background: none; border: none; color: var(--bs-secondary-color); cursor: pointer; font-size: .8rem; padding: 0 .2rem; }
.btn-rm-opt:hover { color: var(--bs-danger); }
.btn-add-opt { background: none; border: var(--bs-border-width) dashed var(--bs-border-color); border-radius: var(--bs-border-radius); padding: .25rem .6rem; font-size: .8rem; color: var(--bs-secondary-color); cursor: pointer; font-family: 'DM Sans',sans-serif; transition: all .15s; }
.btn-add-opt:hover { border-color: var(--bs-primary); color: var(--bs-primary); }
.poll-opt-img-btn { background: none; border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius); padding: .26rem .45rem; font-size: .78rem; color: var(--bs-secondary-color); cursor: pointer; transition: all .15s; flex-shrink: 0; display: inline-flex; align-items: center; }
.poll-opt-img-btn:hover { border-color: var(--bs-primary); color: var(--bs-primary); }
.poll-opt-thumb { display: flex; align-items: center; position: relative; flex-shrink: 0; }
.ann-comment-admin-badge { color: var(--bs-primary); font-size: .65rem; vertical-align: middle; margin: 0 .15rem 0 .1rem; }
/* announcement card */
.ann-card { box-shadow: var(--bs-box-shadow-sm); }
.ann-card-header {
  display: flex; align-items: flex-start; justify-content: space-between;
  margin-bottom: .65rem; gap: .5rem;
}
.ann-card-actions { display: flex; gap: .25rem; align-items: center; flex-shrink: 0; }
.ann-pp-avatar {
  width: 34px; height: 34px; border-radius: 50%; flex-shrink: 0;
  background: linear-gradient(135deg, #203B71, #067BFD); display: flex; align-items: center; justify-content: center;
}
.ann-pp-avatar i { color: #fff; font-size: 1.35rem; }
.ann-author { font-weight: 600; font-size: .88rem; color: var(--bs-emphasis-color); }
.ann-date   { font-size: .76rem; color: var(--bs-secondary-color); margin-top: 1px; }
.ann-body   { font-size: .93rem; color: var(--bs-body-color); line-height: 1.7; margin-bottom: .75rem; }
.ann-body p, .ann-body div { margin: 0 0 .35em; }
.ann-body ul, .ann-body ol { margin: .2em 0 .5em 1.4em; padding: 0; }
.ann-body li { margin-bottom: .15em; }
.ann-body strong, .ann-body b { font-weight: 700; }
.ann-body em, .ann-body i { font-style: italic; }
.ann-body u { text-decoration: underline; }
.ann-body a { color: var(--bs-primary); text-decoration: underline; }
.ann-body a:hover { opacity: .75; }
/* Rendered-markdown extras (marked emits <pre><code>, inline <code>, <img>, gfm <table>). */
.ann-body code { background: var(--bs-tertiary-bg); border: 1px solid var(--bs-border-color); border-radius: 4px; padding: .08em .35em; font-family: 'Courier New', Courier, monospace; font-size: .85em; }
.ann-body pre { background: var(--bs-tertiary-bg); border: 1px solid var(--bs-border-color); border-radius: 7px; padding: .6rem .85rem; margin: .4em 0; overflow-x: auto; }
.ann-body pre code { background: none; border: none; padding: 0; }
.ann-body img { max-width: 100%; height: auto; border-radius: var(--bs-border-radius); margin: .3em 0; }
/* "Aside" images: the copy wraps around a floated image on wide screens; on narrow /
   mobile it stacks (no float, centered) so the text column never gets cramped. Used in
   long announcements (e.g. the Print Studio post) for the smaller, taller shots. */
.ann-body .ann-aside { max-width: 240px; width: 42%; margin: .15em 0 .55rem; border-radius: var(--bs-border-radius); }
.ann-body .ann-aside-right { float: right; margin-left: 1.1rem; }
.ann-body .ann-aside-left  { float: left;  margin-right: 1.1rem; }
.ann-body h2 { clear: both; }   /* each section starts below any prior aside float */
@media (max-width: 560px) {
  .ann-body .ann-aside { float: none; width: 100%; max-width: 320px; margin: .5em auto; display: block; }
}
.ann-body table { border-collapse: collapse; margin: .4em 0; font-size: .9em; }
.ann-body th, .ann-body td { border: 1px solid var(--bs-border-color); padding: .3em .6em; text-align: left; }
.ann-body th { background: var(--bs-tertiary-bg); }

/* ── Markdown editor (announcement composer + per-card edit) ──────────────── */
.md-editor { border: 1px solid var(--bs-border-color); border-radius: var(--bs-border-radius); overflow: hidden; background: var(--bs-body-bg); }
.md-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: .12rem; padding: .35rem .4rem; background: var(--bs-tertiary-bg); border-bottom: 1px solid var(--bs-border-color); }
.md-btn { display: inline-flex; align-items: center; gap: .3rem; background: none; border: 1px solid transparent; border-radius: var(--bs-border-radius-sm); color: var(--bs-body-color); font-size: .85rem; padding: .3rem .45rem; cursor: pointer; line-height: 1; transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t); }
.md-btn:hover { background: var(--bs-secondary-bg); }
.md-btn.active { background: rgba(var(--bs-primary-rgb), .14); border-color: rgba(var(--bs-primary-rgb), .4); color: var(--bs-primary); }
.md-img-btn { margin: 0; }
.md-select { background: var(--bs-body-bg); color: var(--bs-body-color); border: 1px solid var(--bs-border-color); border-radius: var(--bs-border-radius-sm); font-size: .82rem; padding: .25rem .4rem; cursor: pointer; }
.md-sep { width: 1px; align-self: stretch; margin: .2rem .15rem; background: var(--bs-border-color); }
.md-spacer { flex: 1 1 auto; }
.md-textarea { display: block; width: 100%; border: none; outline: none; resize: vertical; min-height: 120px; max-height: 460px; padding: .7rem .85rem; background: var(--bs-body-bg); color: var(--bs-body-color); font-family: 'DM Sans', system-ui, sans-serif; font-size: .93rem; line-height: 1.6; }
.md-textarea::placeholder { color: var(--bs-secondary-color); }
.md-preview { min-height: 120px; padding: .7rem .85rem; margin: 0; }
.md-preview-empty { color: var(--bs-secondary-color); font-size: .9rem; font-style: italic; }

/* ── "Post as" identity selector ─────────────────────────────────────────── */
.ann-postas { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.ann-postas-label { font-size: .72rem; font-weight: 600; text-transform: uppercase; letter-spacing: .03em; color: var(--bs-secondary-color); }
.ann-postas-card { display: inline-flex; align-items: center; gap: .5rem; position: relative; background: var(--bs-body-bg); border: var(--bs-border-width) solid var(--bs-border-color); border-radius: 10px; padding: .3rem .55rem .3rem .4rem; cursor: pointer; text-align: left; transition: border-color .12s, background .12s; }
.ann-postas-card:hover { border-color: var(--bs-secondary-color); }
.ann-postas-card.active { border-color: var(--bs-primary); background: rgba(var(--bs-primary-rgb), .07); }
.ann-postas-avatar { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 50%; overflow: hidden; flex: 0 0 auto; background: var(--bs-tertiary-bg); color: var(--bs-secondary-color); font-size: .8rem; }
.ann-postas-avatar img { width: 100%; height: 100%; object-fit: cover; }
.ann-postas-avatar.pp { background: linear-gradient(135deg, #203B71, #067BFD); color: #fff; }
.ann-postas-meta { display: flex; flex-direction: column; line-height: 1.15; }
.ann-postas-name { font-size: .82rem; font-weight: 600; color: var(--bs-body-color); }
.ann-postas-sub { font-size: .68rem; color: var(--bs-secondary-color); }
.ann-postas-tick { color: var(--bs-primary); font-size: .85rem; opacity: 0; transition: opacity .12s; }
.ann-postas-card.active .ann-postas-tick { opacity: 1; }
/* media grid — editorial mosaic layouts */
.ann-media-grid { display: grid; gap: 3px; border-radius: var(--bs-border-radius); overflow: hidden; margin-bottom: .75rem; }
.ann-grid-img, .ann-grid-more { display: block; width: 100%; height: 100%; }
/* .ann-body img is constrained, but the "+N more" tile holds a bare <img> that
   wasn't — a 720px photo rendered 595px past a 430px phone viewport (clipped,
   so it just looked cut off rather than pushing the page). */
.ann-grid-more img { max-width: 100%; width: 100%; height: 100%; object-fit: cover; }
.ann-grid-img { object-fit: cover; cursor: zoom-in; transition: opacity .15s; }
.ann-grid-img:hover, .ann-grid-more:hover img { opacity: .88; }
.ann-media-grid.grid-1 { grid-template-columns: 1fr; }
.ann-media-grid.grid-1 .ann-grid-img { height: auto; max-height: 400px; object-fit: contain; }
.ann-media-grid.grid-2 { grid-template-columns: 3fr 2fr; aspect-ratio: 5/2; }
.ann-media-grid.grid-3 { grid-template-columns: 3fr 2fr; grid-template-rows: 1fr 1fr; aspect-ratio: 4/3; }
.ann-media-grid.grid-3 :first-child { grid-row: 1 / 3; }
.ann-media-grid.grid-4 { grid-template-areas: "a a b" "c d b"; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr; aspect-ratio: 16/9; }
.ann-media-grid.grid-4 :nth-child(1) { grid-area: a; }
.ann-media-grid.grid-4 :nth-child(2) { grid-area: b; }
.ann-media-grid.grid-4 :nth-child(3) { grid-area: c; }
.ann-media-grid.grid-4 :nth-child(4) { grid-area: d; }
.ann-media-grid.grid-many { grid-template-areas: "a b c" "a d e"; grid-template-columns: 2fr 1fr 1fr; grid-template-rows: 1fr 1fr; aspect-ratio: 4/3; }
.ann-media-grid.grid-many :nth-child(1) { grid-area: a; }
.ann-media-grid.grid-many :nth-child(2) { grid-area: b; }
.ann-media-grid.grid-many :nth-child(3) { grid-area: c; }
.ann-media-grid.grid-many :nth-child(4) { grid-area: d; }
.ann-media-grid.grid-many :nth-child(5) { grid-area: e; }
.ann-grid-more { position: relative; overflow: hidden; cursor: zoom-in; }
.ann-grid-more img { object-fit: cover; transition: opacity .15s; }
.ann-grid-more-overlay { position: absolute; inset: 0; background: rgba(0,0,0,.52); display: flex; align-items: center; justify-content: center; color: #fff; font-size: 1.3rem; font-weight: 700; }
/* lightbox */
#imgLightbox { display:none; position:fixed; inset:0; background:rgba(0,0,0,.94); backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px); z-index:9000; align-items:center; justify-content:center; }
#imgLightbox.show { display:flex; }
#imgLightbox img { max-width:96vw; max-height:92vh; border-radius:var(--bs-border-radius); object-fit:contain; box-shadow:0 10px 50px rgba(0,0,0,.6); user-select:none; image-rendering:-webkit-optimize-contrast; }
.lb-nav { position:fixed; top:50%; transform:translateY(-50%); background:rgba(255,255,255,.15); border:none; color:#fff; font-size:1.1rem; cursor:pointer; border-radius:50%; width:42px; height:42px; display:flex; align-items:center; justify-content:center; z-index:9001; transition:background .15s, transform .15s; }
.lb-nav:hover { background:rgba(255,255,255,.28); transform:translateY(-50%) scale(1.05); }
.lb-prev { left: 16px; }
.lb-next { right: 16px; }
.lb-close { position:fixed; top:14px; right:16px; background:rgba(255,255,255,.15); border:none; color:#fff; font-size:1rem; cursor:pointer; border-radius:50%; width:36px; height:36px; display:flex; align-items:center; justify-content:center; z-index:9001; transition:background .15s, transform .15s; }
.lb-close:hover { background:rgba(255,255,255,.28); transform:scale(1.05); }
.lb-counter { position:fixed; bottom:18px; left:50%; transform:translateX(-50%); color:rgba(255,255,255,.7); font-size:.85rem; z-index:9001; }
/* poll display */
.ann-poll { margin-bottom: .65rem; background: var(--bs-tertiary-bg); border-radius: var(--bs-border-radius-lg); padding: .85rem 1rem; }
.ann-poll-q { font-weight: 700; font-size: .9rem; color: var(--bs-emphasis-color); margin-bottom: .7rem; line-height: 1.3; }
.ann-poll-opts { display: flex; flex-direction: column; gap: .45rem; }
.ann-poll-opt-btn {
  display: flex; align-items: center; gap: .65rem;
  background: var(--bs-body-bg); border: var(--bs-border-width) solid var(--bs-border-color);
  border-radius: 10px; padding: .65rem .85rem;
  text-align: left; font-size: .9rem; font-family: 'DM Sans',sans-serif;
  cursor: pointer; color: var(--bs-body-color);
  min-height: 48px;   /* tap-friendly on mobile */
  transition: border-color .15s, background-color .15s, box-shadow .15s, transform .12s;
}
.ann-poll-opt-btn:hover,
.ann-poll-opt-btn:focus-visible {
  border-color: var(--bs-primary);
  background: color-mix(in srgb, var(--bs-primary) 7%, var(--bs-body-bg));
  outline: none;
}
.ann-poll-opt-btn:active { transform: scale(.985); }
.ann-poll-opt-radio {
  flex-shrink: 0;
  width: 18px; height: 18px;
  border: 2px solid var(--bs-border-color);
  border-radius: 50%;
  background: transparent;
  transition: border-color .15s, background-color .15s;
}
.ann-poll-opt-btn:hover .ann-poll-opt-radio,
.ann-poll-opt-btn:focus-visible .ann-poll-opt-radio { border-color: var(--bs-primary); }
.ann-poll-opt-text { flex: 1; line-height: 1.35; }
/* image poll grid */
.ann-poll-img-grid { display: grid; gap: .5rem; margin-bottom: .4rem; }
.ann-poll-img-grid.cols-2 { grid-template-columns: 1fr 1fr; }
.ann-poll-img-grid.cols-3 { grid-template-columns: 1fr 1fr 1fr; }
.ann-poll-img-tile { background: var(--bs-body-bg); border: var(--bs-border-width) solid var(--bs-border-color); border-radius: 10px; padding: 0; overflow: hidden; cursor: pointer; transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t); text-align: center; display: flex; flex-direction: column; }
.ann-poll-img-tile:hover { border-color: var(--bs-primary); box-shadow: 0 0 0 3px rgba(var(--bs-primary-rgb),.15); }
.ann-poll-img-tile-wrap { aspect-ratio: 1; overflow: hidden; }
.ann-poll-img-tile-img { width: 100%; height: 100%; object-fit: cover; display: block; transition: opacity .15s; }
.ann-poll-img-tile:hover .ann-poll-img-tile-img { opacity: .9; }
.ann-poll-img-placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: var(--bs-secondary-color); font-size: 1.5rem; background: var(--bs-tertiary-bg); }
.ann-poll-img-tile-label { display: block; padding: .4rem .5rem; font-size: .8rem; color: var(--bs-body-color); font-weight: 500; line-height: 1.3; }
/* poll results */
.ann-poll-results { display: flex; flex-direction: column; gap: .4rem; }
.ann-poll-result { margin: 0; }
.ann-poll-bar {
  position: relative;
  height: 38px;
  border-radius: 9px;
  background: var(--bs-body-bg);
  border: 1px solid var(--bs-border-color);
  overflow: hidden;
}
.ann-poll-bar-fill {
  position: absolute; inset: 0 auto 0 0;
  width: 0;   /* set inline by JS */
  background: color-mix(in srgb, var(--bs-primary) 18%, transparent);
  transition: width .55s cubic-bezier(.4, 0, .2, 1);
}
.ann-poll-result.my-pick .ann-poll-bar { border-color: color-mix(in srgb, var(--bs-success) 60%, transparent); }
.ann-poll-result.my-pick .ann-poll-bar-fill {
  background: color-mix(in srgb, var(--bs-success) 22%, transparent);
}
.ann-poll-bar-content {
  position: relative; z-index: 1;
  display: flex; align-items: center; justify-content: space-between;
  height: 100%;
  padding: 0 .8rem;
  gap: .5rem;
}
.ann-poll-bar-label {
  display: flex; align-items: center; gap: .5rem;
  min-width: 0; flex: 1;
  font-size: .88rem; line-height: 1.2;
  color: var(--bs-body-color);
}
.ann-poll-bar-text {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ann-poll-result.my-pick .ann-poll-bar-text { font-weight: 600; color: var(--bs-emphasis-color); }
.ann-poll-bar-pct {
  flex-shrink: 0;
  font-size: .8rem; font-weight: 600;
  color: var(--bs-secondary-color);
  font-variant-numeric: tabular-nums;
}
.ann-poll-result.my-pick .ann-poll-bar-pct { color: var(--bs-success); }
.ann-poll-check {
  font-size: .8rem;
  color: var(--bs-success);
  flex-shrink: 0;
}
.ann-poll-result-img {
  width: 26px; height: 26px;
  object-fit: cover; border-radius: var(--bs-border-radius-sm);
  flex-shrink: 0; cursor: zoom-in;
}

.ann-poll-footer {
  display: flex; justify-content: space-between; align-items: center;
  flex-wrap: wrap; gap: .5rem;
  font-size: .78rem; color: var(--bs-secondary-color);
  margin-top: .65rem;
}
.ann-poll-footer-total { font-weight: 500; }
.ann-poll-footer-actions { display: flex; gap: .35rem; flex-wrap: wrap; }
.ann-poll-footer-btn {
  background: transparent; border: 1px solid var(--bs-border-color);
  border-radius: 999px;
  padding: .25rem .65rem;
  font-size: .75rem; color: var(--bs-secondary-color);
  cursor: pointer;
  transition: border-color .15s, color .15s, background-color .15s;
}
.ann-poll-footer-btn:hover,
.ann-poll-footer-btn:focus-visible {
  border-color: var(--bs-primary);
  color: var(--bs-primary);
  background: color-mix(in srgb, var(--bs-primary) 6%, transparent);
  outline: none;
}
/* Bootstrap progress override for polls */
.progress { --bs-progress-height: 6px; --bs-progress-bg: var(--bs-border-color); --bs-progress-bar-bg: var(--bs-primary); border-radius: 999px; }
.progress-bar { transition: width .4s ease; }
/* Voter list, grouped by option. Each voter is a chip with avatar +
   name; staff get a clickable chip that opens the affiliate's profile. */
.ann-poll-voters-list {
  margin-top: .8rem; padding-top: .65rem;
  border-top: 1px dashed var(--bs-border-color);
  display: flex; flex-direction: column; gap: .55rem;
}
.ann-poll-voters-empty {
  font-style: italic; font-size: .8rem; color: var(--bs-secondary-color);
}
.ann-poll-voters-group { display: flex; flex-direction: column; gap: .3rem; }
.ann-poll-voters-opt {
  font-size: .72rem; font-weight: 600;
  color: var(--bs-secondary-color);
  text-transform: uppercase; letter-spacing: .04em;
  display: flex; align-items: center; gap: .35rem;
}
.ann-poll-voters-count {
  /* Aether: tinted like the sidebar count badges, not solid gray */
  background: rgba(var(--bs-primary-rgb), .12);
  color: var(--bs-primary);
  font-size: .68rem; font-weight: 600;
  padding: .05rem .4rem;
  border-radius: 999px;
}
.ann-poll-voters-chips {
  display: flex; flex-wrap: wrap; gap: .3rem;
}
.ann-poll-voter-chip {
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .2rem .55rem .2rem .25rem;
  background: var(--bs-body-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: 999px;
  font-size: .78rem;
  color: var(--bs-body-color);
  line-height: 1.2;
}
.ann-poll-voter-chip > .pp-avatar,
.ann-poll-voter-chip > img.pp-avatar {
  flex-shrink: 0;
}
.ann-poll-voter-chip.clickable {
  cursor: pointer;
  transition: border-color .12s, background-color .12s, transform .12s;
}
.ann-poll-voter-chip.clickable:hover,
.ann-poll-voter-chip.clickable:focus-visible {
  border-color: var(--bs-primary);
  background: color-mix(in srgb, var(--bs-primary) 8%, var(--bs-body-bg));
  outline: none;
}
.ann-poll-voter-chip.clickable:active { transform: scale(.97); }
/* card footer / action buttons */
.ann-actions-footer { display: flex; align-items: center; gap: .1rem; padding: .4rem .75rem; background: transparent; }
.btn-ann-like, .btn-ann-comment {
  background: none; border: none;
  padding: .3rem .55rem; font-size: .8rem; cursor: pointer; color: var(--bs-secondary-color);
  font-family: 'DM Sans', sans-serif; display: flex; align-items: center; gap: .35rem;
  border-radius: var(--bs-border-radius); transition: all .15s; font-weight: 500;
}
.btn-ann-like:hover { background: rgba(232,121,249,.1); color: var(--pink); }
.btn-ann-like.liked { color: var(--pink); background: rgba(232,121,249,.1); }
.btn-ann-comment:hover { background: var(--bs-tertiary-bg); color: var(--bs-body-color); }
.btn-ann-icon {
  background: none; border: none; color: var(--bs-secondary-color); cursor: pointer;
  font-size: .78rem; padding: .1rem .3rem; transition: color .15s; flex-shrink: 0;
}
.btn-ann-icon:hover { color: var(--bs-primary); }
.btn-ann-icon.del:hover { color: var(--bs-danger); }
.btn-ann-icon-yes:hover { color: #16a34a; }
.ann-del-confirm-label { font-size: .75rem; color: var(--bs-secondary-color); white-space: nowrap; }
/* rich text editor toolbar */
.ann-rte-toolbar { display: flex; align-items: center; gap: .15rem; flex-wrap: wrap; border-bottom: 1px solid var(--bs-border-color); padding-bottom: .4rem; margin-bottom: .25rem; }
.ann-rte-btn { background: none; border: none; border-radius: var(--bs-border-radius-sm); padding: .25rem .45rem; font-size: .85rem; color: var(--bs-secondary-color); cursor: pointer; line-height: 1; transition: background .12s, color .12s; }
.ann-rte-btn:hover { background: var(--bs-tertiary-bg); color: var(--bs-emphasis-color); }
.ann-rte-sep { display: inline-block; width: 1px; height: 1.1rem; background: var(--bs-border-color); margin: 0 .2rem; flex-shrink: 0; }
.ann-rte-select { background: var(--bs-body-bg); border: 1px solid var(--bs-border-color); border-radius: var(--bs-border-radius-sm); padding: .2rem .3rem; font-size: .78rem; color: var(--bs-secondary-color); cursor: pointer; font-family: 'DM Sans',sans-serif; }
.ann-rte-select:hover { border-color: var(--bs-emphasis-color); color: var(--bs-emphasis-color); }
.ann-rte-color-label { cursor: pointer; display: inline-flex !important; align-items: center; gap: .25rem; }
.ann-rte-color-input { width: 24px; height: 24px; padding: 0; border: 1px solid var(--bs-border-color); border-radius: var(--bs-border-radius-sm); cursor: pointer; background: none; }
.ann-rte-body { min-height: 72px; max-height: 320px; overflow-y: auto; font-family: 'DM Sans',sans-serif; font-size: .93rem; line-height: 1.7; color: var(--bs-body-color); outline: none; }
.ann-rte-body:empty::before { content: attr(data-placeholder); color: var(--bs-secondary-color); pointer-events: none; }
.ann-rte-body ul, .ann-rte-body ol { margin: .2em 0 .4em 1.4em; padding: 0; }
.ann-rte-body li { margin-bottom: .1em; }
.ann-rte-body a { color: var(--bs-primary); text-decoration: underline; }
/* rte + display: block-level content */
.ann-rte-body h1, .ann-body h1 { font-size: 1.45rem; font-weight: 700; margin: .2em 0 .3em; line-height: 1.25; }
.ann-rte-body h2, .ann-body h2 { font-size: 1.2rem; font-weight: 700; margin: .2em 0 .3em; line-height: 1.3; }
.ann-rte-body h3, .ann-body h3 { font-size: 1rem; font-weight: 700; margin: .2em 0 .25em; line-height: 1.35; }
.ann-rte-body blockquote, .ann-body blockquote { border-left: 3px solid var(--bs-primary); margin: .4em 0 .4em .4em; padding: .2em .75em; color: var(--bs-secondary-color); font-style: italic; }
.ann-rte-body pre.ann-code-block, .ann-body pre.ann-code-block { background: var(--bs-tertiary-bg); border: 1px solid var(--bs-border-color); border-radius: 6px; padding: .55rem .85rem; margin: .35em 0; overflow-x: auto; font-family: 'Courier New',Courier,monospace; font-size: .83em; line-height: 1.55; white-space: pre-wrap; word-break: break-word; }
.ann-rte-body pre.ann-code-block code, .ann-body pre.ann-code-block code { font-family: inherit; background: none; }
.ann-rte-body aside.ann-aside, .ann-body aside.ann-aside { background: rgba(var(--bs-primary-rgb),.07); border-left: 3px solid var(--bs-primary); border-radius: 0 7px 7px 0; padding: .45rem .85rem; margin: .35em 0; font-size: .9rem; }
.ann-rte-body hr, .ann-body hr { border: none; border-top: var(--bs-border-width) solid var(--bs-border-color); margin: .7em 0; }
.ann-rte-body s, .ann-rte-body strike, .ann-body s, .ann-body strike { text-decoration: line-through; }
.ann-rte-body .ann-text-sm, .ann-body .ann-text-sm { font-size: .78em; }
.ann-rte-body .ann-text-lg, .ann-body .ann-text-lg { font-size: 1.15em; }
.ann-rte-body .ann-text-xl, .ann-body .ann-text-xl { font-size: 1.35em; }
/* edit mode */
.ann-edit-area { display: flex; flex-direction: column; gap: .45rem; }
.ann-rte-wrapper { border: var(--bs-border-width) solid var(--bs-primary); border-radius: var(--bs-border-radius); overflow: hidden; }
.ann-rte-wrapper .ann-rte-toolbar { border-bottom: 1px solid var(--bs-border-color); margin-bottom: 0; border-radius: 0; padding: .3rem .5rem; }
.ann-edit-textarea { width: 100%; border: none; padding: .6rem .85rem; font-family: 'DM Sans',sans-serif; font-size: .9rem; background: var(--bs-body-bg); color: var(--bs-body-color); outline: none; min-height: 80px; }
.ann-edit-media { display: flex; flex-wrap: wrap; gap: .4rem; }
.ann-edit-thumb { position: relative; width: 64px; height: 64px; border-radius: var(--bs-border-radius-sm); overflow: hidden; flex-shrink: 0; }
.ann-edit-thumb img { width: 100%; height: 100%; object-fit: cover; }
.ann-edit-thumb .rm-btn { position: absolute; top: 2px; right: 2px; background: rgba(0,0,0,.55); border: none; border-radius: 50%; width: 24px; height: 24px; color: #fff; cursor: pointer; font-size: .7rem; display: flex; align-items: center; justify-content: center; }
.ann-edit-footer { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; margin-top: .2rem; }
.ann-edit-footer .btn-sm { padding: .3rem .75rem; font-size: .8rem; border-radius: 7px; border: var(--bs-border-width) solid var(--bs-border-color); background: var(--bs-body-bg); color: var(--bs-body-color); cursor: pointer; font-family: 'DM Sans',sans-serif; transition: all .15s; }
.ann-edit-footer .btn-sm.primary { background: var(--bs-primary); color: var(--pp-on-primary, #fff); border-color: var(--bs-primary); }
.ann-edit-footer .btn-sm:hover:not(.primary) { border-color: var(--bs-secondary-color); }
/* comments */
.ann-comments-section { border-top: 1px solid var(--bs-border-color); }
.ann-comment { align-items: flex-start; }
.ann-comment-author { font-weight: 600; font-size: .82rem; color: var(--bs-emphasis-color); }
/* Wraps the avatar + author name on a comment when staff can click through
   to that affiliate's profile. Looks identical to the static text/avatar
   when idle; lights up on hover/focus. */
.ann-comment-author-link {
  background: none; border: 0; padding: 0; color: inherit;
  font: inherit; line-height: inherit; text-align: left;
  cursor: pointer; border-radius: 4px;
  transition: opacity .12s ease, background-color .12s ease;
}
.ann-comment-author-link:hover,
.ann-comment-author-link:focus-visible {
  opacity: .75; outline: none;
}
.ann-comment-author-link:focus-visible {
  background: color-mix(in srgb, var(--bs-primary) 12%, transparent);
}
.ann-comment-date   { font-size: .72rem; color: var(--bs-secondary-color); }
.ann-comment-body   { font-size: .85rem; color: var(--bs-body-color); margin-top: .1rem; }
.ann-comment-del { background: none; border: none; color: var(--bs-secondary-color); cursor: pointer; font-size: .7rem; padding: 0 .15rem; transition: color .15s; line-height: 1; }
.ann-comment-del:hover { color: var(--bs-danger); }
.flex-1 { flex: 1; }
.min-w-0 { min-width: 0; }
.ann-comment-input {
  width: 100%; border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius);
  padding: .4rem .7rem; font-size: .85rem; font-family: 'DM Sans', sans-serif;
  background: var(--bs-body-bg); color: var(--bs-body-color);
  outline: none; transition: border-color .2s; margin-top: .45rem;
}
.ann-comment-input:focus { border-color: var(--bs-primary); }

/* ── Attach buttons in announcement composer ── */
.ann-attach-btn {
  background: none; border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius);
  padding: .22rem .55rem; font-size: .78rem; color: var(--bs-secondary-color);
  font-family: 'DM Sans', sans-serif; cursor: pointer; transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t);
  display: inline-flex; align-items: center; gap: .3rem; white-space: nowrap;
}
.ann-attach-btn:hover { border-color: var(--bs-primary); color: var(--bs-primary); }
.ann-attach-btn.active { background: rgba(var(--bs-primary-rgb),.08); border-color: var(--bs-primary); color: var(--bs-primary); }

/* ── Upload banner btn ── */
.btn-upload-banner {
  background: none; border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius);
  padding: .38rem .55rem; font-size: .72rem; cursor: pointer; color: var(--bs-secondary-color);
  font-family: 'DM Sans', sans-serif; transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t); white-space: nowrap;
  display: flex; align-items: center; gap: .25rem;
}
.btn-upload-banner:hover { border-color: var(--bs-primary); color: var(--bs-primary); }

/* ── ctxSplit columns: height:100% bounds them to the row so overflow-auto scrolls (sticky fix) ── */
#ctxSplit > * { min-height: 0; }
@media (min-width: 992px) {
  #ctxSplit > * { height: 100%; }
}

/* ── Admin breadcrumb bar (sticky, shared across affiliates/detail/client pages) ── */
.admin-crumb-bar {
  position: sticky; top: 0; z-index: 10;
  /* Scrolls over tinted page content — must match the page tone, not surface
     white, or it reads as a seam (same reasoning as the help-toc mask). */
  background: var(--pp-page-bg);
  border-bottom: 1px solid var(--bs-border-color);
  padding: .5rem 1rem;
  display: flex; align-items: center; justify-content: space-between; gap: .5rem; flex-wrap: wrap;
}
.admin-crumb-bar .breadcrumb-item a {
  color: var(--bs-secondary-color);
  text-decoration: none;
  font-weight: 600;
}
.admin-crumb-bar .breadcrumb-item a:hover {
  color: var(--bs-body-color);
}

/* ── Admin affiliate panel ── */
.admin-panel-card {
  background: var(--bs-body-bg); border-radius: var(--bs-border-radius-lg); border: 1px solid var(--bs-border-color);
  box-shadow: 0 1px 3px rgba(0,0,0,.04);
  padding: 1.4rem; margin-bottom: 1rem;
}
.admin-panel-card h6 {
  font-weight: 700; font-size: .7rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--bs-secondary-color); margin-bottom: 1rem; display: flex; align-items: center; gap: .4rem;
}
.aff-client-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: .65rem .75rem; border-radius: var(--bs-border-radius-sm); cursor: pointer; gap: .5rem;
  transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t);
}
.aff-client-row:hover { background: rgba(var(--bs-primary-rgb),.08); }
.aff-client-row-main { display: flex; align-items: center; gap: .5rem; min-width: 0; }
.aff-client-badges { display: flex; align-items: center; gap: .3rem; flex-shrink: 0; }

/* ── Scrollbar ── */
.clients-section::-webkit-scrollbar { width: 4px; }
.clients-section::-webkit-scrollbar-thumb { background: var(--bs-border-color); border-radius: 2px; }

/* ── JS-toggled panels: flex-direction without !important so JS style.display works ── */
#myClientsManager,
#ordersManager,
#affiliatesManager,
#allClientsManager,
#partiesManager,
#themeManager,
#changelogManager { flex-direction: column; }

/* ── Back button (hidden on desktop, shown on mobile) ── */
.mobile-back-btn { display: none; }

/* ── Version button in sidebar footer ── */
.btn-version-link {
  background: none; border: none; padding: 0;
  font-size: .7rem; color: var(--bs-secondary-color);
  cursor: pointer; font-family: 'DM Sans', sans-serif;
  transition: color .15s;
}
.btn-version-link:hover { color: var(--bs-primary); }

/* ── Post-as toggle ── */
.post-as-btn {
  display: flex; align-items: center; gap: .3rem;
  background: none; border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius);
  padding: .22rem .6rem; font-size: .77rem; color: var(--bs-secondary-color);
  font-family: 'DM Sans', sans-serif; cursor: pointer; transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t);
  white-space: nowrap;
}
.post-as-btn.active { background: rgba(var(--bs-primary-rgb),.08); border-color: var(--bs-primary); color: var(--bs-primary); font-weight: 600; }
.post-as-btn:hover:not(.active) { border-color: var(--bs-emphasis-color); color: var(--bs-emphasis-color); }

/* ── Bootstrap offcanvas-lg desktop reset overrides ── */
@media (min-width: 992px) {
  .sidebar {
    width: 240px !important;
    min-width: 240px !important;
    background-color: var(--bs-body-bg) !important;
    /* Aether: the white-chrome-vs-tinted-page contrast draws this seam now;
       keep the 1px for layout stability but paint it transparent. */
    border-right: 1px solid transparent !important;
    flex-shrink: 0 !important;
    overflow: hidden !important;
    transition: width .18s ease, min-width .18s ease !important;
  }
  .sidebar.sidebar-collapsed {
    width: 56px !important;
    min-width: 56px !important;
  }
}

/* ── Sidebar logout row ── */
/* Shown at all widths so the logout button is reachable on mobile/tablet too
   (was previously desktop-only ≥992px, which hid logout from narrow screens). */
.sidebar-collapse-row { display: flex; border-top: 1px solid var(--bs-border-color); flex-shrink: 0; }
/* Left edge matches the nav rows above (--pp-rail-x); the icon+label gap
   matches theirs too, so Log Out reads as the last item in the same list
   rather than a differently-inset one. */
.sidebar-collapse-btn { display: flex; align-items: center; gap: .45rem; width: 100%; padding: .5rem var(--pp-rail-x); border: none; background: none; color: var(--bs-secondary-color); font-size: .8rem; cursor: pointer; text-align: left; }
.sidebar-collapse-btn:hover { background: var(--bs-tertiary-bg); color: var(--bs-body-color); }
.sidebar-logout-btn { color: var(--bs-danger); }
.sidebar-logout-btn:hover { background: rgba(220,53,69,.08); color: var(--bs-danger); }
.sidebar-collapse-label { font-size: .78rem; }

/* ── Filter / search rows ──
   Bootstrap's -sm controls land at 31px but a -sm button wrapping only a
   FontAwesome glyph came out at 37px (the icon's own line box, not the
   button's padding), so search sat visibly shorter than everything beside it.
   One height for every control in the row instead of trusting the defaults. */
.pp-filter-row, .pp-panel-head { --pp-ctl-h: 36px; }
.pp-filter-row .input-group,  .pp-panel-head .input-group,
.pp-filter-row .form-control, .pp-panel-head .form-control,
.pp-filter-row .form-select,  .pp-panel-head .form-select,
.pp-filter-row .btn,          .pp-panel-head .btn { height: var(--pp-ctl-h); }
.pp-filter-row .btn, .pp-panel-head .btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: .3rem; line-height: 1;
}
/* Panel headers only differ in what they hold — a title-only one (My Account)
   measured 36px against 54px everywhere else. Pin the row, not its contents. */
.pp-panel-head { min-height: 54px; }
/* The list pages used to carry a second row holding just a search box, which
   read as a band of dead white between the header and the table (Jeff) — while
   the header itself had an empty middle. Merged: the search fills that middle
   and the page's actions stay pinned right. */
.pp-panel-head:has(.input-group) { justify-content: flex-start !important; }
/* Everything but the title rides the right edge as one cluster (Jeff). The
   auto margin is on the TITLE, not the last item — the trailing element is
   often display:none (admin-only refresh, affiliate-only tabs), and an auto
   margin on a hidden box pushes nothing. */
.pp-panel-head:has(.input-group) > .pp-panel-title { margin-right: auto; }
.pp-panel-head .input-group { flex: 0 1 auto; }
/* Orders' control groups are nested wrappers (the affiliate and admin variants
   JS swaps between). Wrapping broke them onto a second line inside the header —
   97px instead of 54. The fix is in the markup: Bootstrap's .flex-wrap is
   `!important`, so a stylesheet rule here could never have won. */

/* ── Announcement composer collapse ──
   Collapsed by default (Jeff). The grid 0fr→1fr trick animates to the content's
   natural height without hardcoding a max — the inner wrapper owns the
   overflow:hidden that makes it clip while collapsing. */
.ann-compose-head {
  display: flex; align-items: center; gap: .55rem; width: 100%;
  padding: .7rem .95rem; border: none; background: none;
  color: var(--bs-body-color); font-size: .9rem; font-weight: 600;
  cursor: pointer; text-align: left;
  border-radius: var(--bs-border-radius);
  transition: background-color var(--pp-t);
}
.ann-compose-head:hover { background: var(--bs-tertiary-bg); }
.ann-compose-head > .fa-bullhorn { color: var(--bs-primary); font-size: .85rem; }
.ann-compose-head-label { flex: 1 1 auto; min-width: 0; }
.ann-compose-chev {
  flex-shrink: 0; font-size: .75rem; color: var(--bs-secondary-color);
  transition: transform .25s cubic-bezier(.4,0,.2,1);
}
.ann-composer-open .ann-compose-chev { transform: rotate(180deg); }
.ann-compose-body {
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows .28s cubic-bezier(.4,0,.2,1);
}
.ann-composer-open .ann-compose-body { grid-template-rows: 1fr; }
.ann-compose-inner { overflow: hidden; min-height: 0; }
.ann-composer-open .ann-compose-head {
  border-bottom: var(--bs-border-width) solid var(--bs-border-color);
  border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0;
}
@media (prefers-reduced-motion: reduce) {
  .ann-compose-body, .ann-compose-chev { transition: none; }
}

/* Portal update banner — non-blocking "new version available" pill, bottom
   center. Inverted fill (dark pill on light theme, light pill on dark) so it
   reads as a system notice rather than page content. */
.portal-update-banner {
  position: fixed;
  left: 50%; bottom: 20px;
  transform: translateX(-50%);
  z-index: 2000;
  display: flex;
  align-items: center;
  gap: .7rem;
  max-width: calc(100vw - 32px);
  padding: .5rem .6rem .5rem 1rem;
  background: var(--bs-emphasis-color, #1f2430);
  color: var(--bs-body-bg, #fff);
  border-radius: 999px;
  box-shadow: 0 8px 28px rgba(0,0,0,.28);
  font-size: .85rem;
  animation: pub-rise .3s ease;
}
@keyframes pub-rise { from { opacity: 0; transform: translate(-50%, 12px); } to { opacity: 1; transform: translate(-50%, 0); } }
.portal-update-banner i { margin-right: .4rem; opacity: .85; }
.portal-update-banner .pub-refresh {
  border: none;
  background: var(--bs-primary, #4f8cff);
  color: #fff;
  font-weight: 600;
  font-size: .82rem;
  padding: .35rem .9rem;
  border-radius: 999px;
  cursor: pointer;
  white-space: nowrap;
}
.portal-update-banner .pub-refresh:hover { filter: brightness(1.08); }
.portal-update-banner .pub-dismiss {
  border: none;
  background: none;
  color: inherit;
  opacity: .6;
  font-size: 1.2rem;
  line-height: 1;
  padding: 0 .35rem;
  cursor: pointer;
}
.portal-update-banner .pub-dismiss:hover { opacity: 1; }
.portal-update-banner .pub-msg { display: flex; align-items: center; min-width: 0; }
/* Phones: a centred pill that's wider than the screen just gets cropped, so it
   becomes an inset bar instead — full width minus a gutter, clear of the home
   indicator, and its own keyframes (the shared one bakes in translateX(-50%)). */
@media (max-width: 600px) {
  .portal-update-banner {
    left: 12px; right: 12px; transform: none; max-width: none;
    bottom: calc(12px + env(safe-area-inset-bottom));
    gap: .5rem; padding: .6rem .6rem .6rem .9rem;
    font-size: .82rem; border-radius: var(--bs-border-radius-lg);
    animation: pub-rise-m .3s ease;
  }
  .portal-update-banner .pub-msg { flex: 1 1 auto; }
  .portal-update-banner .pub-refresh { padding: .35rem .8rem; }
}
@keyframes pub-rise-m { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }

/* Top-right collapse affordance (desktop only) */
.sidebar-collapse-header { flex-shrink: 0; }
.sidebar-collapse-corner { border: none; background: none; color: var(--bs-secondary-color); padding: .25rem .45rem; border-radius: var(--bs-border-radius-sm); cursor: pointer; font-size: .85rem; line-height: 1; }
.sidebar-collapse-corner:hover { background: var(--bs-tertiary-bg); color: var(--bs-body-color); }

.my-events-entry > span { white-space: nowrap; overflow: hidden; min-width: 0; text-overflow: ellipsis; }

/* ── Collapsed sidebar: icon-only rail ──
   DESKTOP ONLY. The collapse toggles are desktop-only (d-lg-flex), but the
   `sidebarCollapsed` pref persists and is re-applied at every width on boot,
   and the place-card designer auto-collapses too. If these label-hiding rules
   weren't gated to ≥992px, opening the mobile hamburger drawer (which IS
   full-width) would show centered icons with font-size:0 labels — i.e. the
   words never appear. Gating to desktop keeps the mobile drawer always-expanded. */
@media (min-width: 992px) {
  .sidebar-collapsed .sidebar-collapse-label { display: none; }
  .sidebar-collapsed .sidebar-collapse-btn { justify-content: center; padding: .5rem .25rem; }
  .sidebar-collapsed .sidebar-collapse-corner { padding: .25rem; }
  .sidebar-collapsed .sidebar-collapse-corner i { transform: rotate(180deg); }
  .sidebar-collapsed .sidebar-collapse-header { justify-content: center !important; }
  .sidebar-collapsed .sidebar-profile-info,
  .sidebar-collapsed .sidebar-profile-arrow,
  .sidebar-collapsed .sidebar-footer-text { display: none; }
  .sidebar-collapsed .sidebar-profile { justify-content: center; padding-left: 0; padding-right: 0; }
  .sidebar-collapsed footer { padding-left: 0 !important; padding-right: 0 !important; }
  .sidebar-collapsed footer > div { justify-content: center; }
  .sidebar-collapsed .my-events-entry { justify-content: center; padding-left: 0; padding-right: 0; }
  .sidebar-collapsed .my-events-entry > span { font-size: 0; display: flex; justify-content: center; }
  .sidebar-collapsed .my-events-entry > span > i { font-size: .85rem !important; margin: 0; }
  .sidebar-collapsed .my-events-entry .badge { display: none; }
}

/* ── Mobile menu (<992px) ──
   In-flow, NOT a fixed drawer. iOS 26/27 Safari samples the background of any
   position:fixed/sticky element for its toolbar + safe-area tint, so a fixed
   drawer/backdrop hijacked the chin colour and froze it against theme changes.
   As a normal block the menu can't be sampled — the chin keeps tracking the
   page. Open = the menu takes over the content region (see #mainPanel below). */
@media (max-width: 991.98px) {
  .sidebar { display: none !important; width: 100%; }
  body.pp-menu-open .sidebar { display: flex !important; }
  body.pp-menu-open #mainPanel { display: none !important; }
  /* The menu takes the whole screen INCLUDING the strip — its own header
     replaces it, with the close button sitting where the hamburger was. */
  body.pp-menu-open .pp-mobilebar { display: none !important; }
  /* Open/close animation. Closing is a separate class because the panel has to
     stay mounted through the slide-out; portal.js drops pp-menu-open after
     MENU_CLOSE_MS (keep the two in sync). */
  body.pp-menu-open .sidebar { animation: ppMenuIn .22s cubic-bezier(.22,.8,.3,1); }
  body.pp-menu-closing .sidebar { animation: ppMenuOut .19s ease-in forwards; }
  @keyframes ppMenuIn  { from { opacity: 0; transform: translateX(-16px); } to { opacity: 1; transform: none; } }
  @keyframes ppMenuOut { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateX(-16px); } }
  @media (prefers-reduced-motion: reduce) {
    body.pp-menu-open .sidebar, body.pp-menu-closing .sidebar { animation: none; }
  }
  #appShell > .d-flex.flex-grow-1 { overflow: visible !important; }
  /* The menu is a self-contained SCREEN, not a document: exactly one viewport
     tall, with the nav list as the only scroller between a header and footer
     that therefore can't scroll away on their own. Letting the ROOT scroll it
     instead (the previous approach) pushed the menu past the bottom of the
     page with the footer sitting over the cut-off rows, so there was no way to
     tell more nav existed — and no fixed/sticky element is needed here, which
     keeps Safari sampling the page for its chrome tint. */
  body.pp-menu-open .sidebar {
    height: 100dvh; max-height: 100dvh; overflow: hidden !important;
  }
  /* ⚠ The scroller is itself a column flex container, so its children are flex
     items that SHRINK by default — without this the nav silently compressed to
     fit (measured: 719px of content squashed into a 467px box) instead of
     scrolling, and the scroll shadows correctly showed nothing because there
     was nothing to scroll. */
  body.pp-menu-open #normalSidebarContent > * { flex-shrink: 0; }
  body.pp-menu-open #normalSidebarContent {
    flex: 1 1 auto; min-height: 0;
    overflow-y: auto !important; overflow-x: hidden !important;
    overscroll-behavior: contain;
    /* Self-detecting scroll shadows: the two `local` cover gradients travel
       with the content and uncover the `scroll` shadows underneath ONLY when
       there is somewhere to scroll — so a nav that fits shows nothing.
       ⚠ The covers MUST paint the sidebar's own background, not --bs-body-bg:
       in dark mode .sidebar is --bs-tertiary-bg, so a body-bg cover painted a
       darker band that read as a permanent drop shadow above Knowledge Base
       (Jeff). Softer and shorter too — it's a hint, not a divider. */
    background:
      linear-gradient(var(--pp-sidebar-bg) 40%, rgba(0,0,0,0)) top    / 100% 22px no-repeat local,
      linear-gradient(rgba(0,0,0,0), var(--pp-sidebar-bg) 60%) bottom / 100% 22px no-repeat local,
      radial-gradient(farthest-side at 50% 0,    rgba(0,0,0,.07), rgba(0,0,0,0)) top    / 100% 7px no-repeat scroll,
      radial-gradient(farthest-side at 50% 100%, rgba(0,0,0,.07), rgba(0,0,0,0)) bottom / 100% 7px no-repeat scroll;
  }
  /* Trim so the nav usually fits outright. The search pill STAYS: the strip's
     magnifier gave up its slot to the page's primary action, so the sidebar is
     now the only way in to search (Jeff). */
  body.pp-menu-open .pp-omnisearch--side { margin: .45rem .75rem .25rem; }
  body.pp-menu-open .my-events-entry { padding-top: .45rem; padding-bottom: .45rem; }
  body.pp-menu-open .sidebar-profile { padding: .7rem 1.1rem; }
  body.pp-menu-open .sidebar-admin-label { padding-top: .55rem; }
  /* Counts read as part of the row, not as a right-hand column (Jeff). */
  body.pp-menu-open .my-events-entry { justify-content: flex-start; gap: .5rem; }
  body.pp-menu-open .my-events-entry > span:first-child { flex: 0 1 auto; }
  body.pp-menu-open .my-events-entry > .badge { flex-shrink: 0; }
  /* Header + footer are plain flex siblings of the scroller — no sticky needed
     now that the menu is viewport-height, which also means Safari has no
     sticky element to sample for its chrome tint. */
  /* Geometry mirrors .pp-mobilebar exactly (same 48px + inset, same .5rem left
     gutter, same 38px .pp-strip-btn) so the close button lands on the very
     pixel the hamburger occupied — the strip is hidden while this is up. */
  .sidebar-mobile-hdr {
    display: flex; align-items: center; gap: .35rem;
    padding: 0 .75rem 0 .5rem; flex-shrink: 0;
    padding-top: env(safe-area-inset-top);
    min-height: calc(48px + env(safe-area-inset-top));
    background: var(--bs-body-bg);
  }
  .sidebar-mobile-hdr .sidebar-brand-name { font-size: 1rem; }
  body.pp-menu-open .sidebar-collapse-row,
  body.pp-menu-open .sidebar-foot { background: var(--bs-body-bg); }
  /* Roomier on phones so the identity + theme switch aren't jammed against the
     screen edge / home indicator (Jeff). */
  body.pp-menu-open .sidebar-foot {
    padding: .8rem 1.1rem calc(.9rem + env(safe-area-inset-bottom));
    gap: .75rem;
  }
  body.pp-menu-open .sidebar-collapse-btn { padding: .55rem 1.1rem; }
  /* Old drawer sizing kept for the panels that ARE still real overlays */
  .sidebar {
    height: auto;
  }

  /* ── Table screens own their scroll ──
     `.affmgr-table thead th` is already position:sticky, but sticky pins to the
     nearest SCROLLPORT — and .affmgr-table-wrap scrolls horizontally, so it is
     that scrollport. Under root scrolling the wrap grew to its full content
     height (measured 9634px in a 932px viewport), so it never scrolled
     vertically and the "sticky" header just rode the page off screen.
     Bounding the panel to one viewport hands the vertical scroll back to the
     wrap, which is what makes the header actually pin. Panels are uniformly
     #mainPanel > .h-100 > .affmgr-table-wrap; browsers without :has() simply
     drop the rule and keep the old behaviour. */
  /* !important is required, not defensive: these panels carry Bootstrap's
     .h-100, which is `height: 100% !important` and otherwise wins. */
  #mainPanel > .h-100:has(> .affmgr-table-wrap) {
    display: flex; flex-direction: column; min-height: 0;
    height: calc(100dvh - 48px - env(safe-area-inset-top)
                 - env(safe-area-inset-bottom) - 1.25rem) !important;
  }
  #mainPanel > .h-100 > .affmgr-table-wrap { flex: 1 1 auto; min-height: 0; }
  /* Everything above the table keeps its height. As flex items of the
     viewport-height column they shrink by default, so a header that wrapped to
     two lines got squeezed back to its 54px min-height and its own contents
     spilled over the table (measured: 80px of controls in a 54px box). */
  #mainPanel > .h-100 > :not(.affmgr-table-wrap) { flex-shrink: 0; }

  /* ── Sub-page header row is redundant on phones ──
     The strip already names the page, so the title block and its count move up
     there, along with the page's one primary action. A row left with nothing
     but those collapses (portal.js measures rather than hardcoding which). */
  .pp-panel-title { display: none !important; }
  .pp-panel-head [data-strip-action] { display: none !important; }
  /* ── List-page header on a phone ──
     The nowrap that keeps Orders on one line at desktop widths ran it 556px
     wide inside a 430px viewport — the sort and column controls were simply
     off-screen. Below lg it wraps into a deliberate two-line block instead:
     search owns the first line, the filters share the second evenly. The
     !importants are unavoidable — the inline max-width/width on these controls
     is what sizes them on desktop. */
  .pp-panel-head > .d-flex { flex: 1 1 100%; }
  .pp-panel-head, .pp-panel-head > .d-flex { flex-wrap: wrap; row-gap: .5rem; column-gap: .4rem; }
  .pp-panel-head .input-group {
    flex: 1 1 100%; width: 100% !important; max-width: none !important; min-width: 0;
  }
  .pp-panel-head .form-select {
    flex: 1 1 0; width: auto !important; min-width: 0; max-width: none !important;
  }
  .pp-panel-head .dropdown, .pp-panel-head > .btn { flex: 0 0 auto; }
  .pp-panel-head.pp-head-empty { display: none !important; }
  /* A dropdown can't be proxied into the strip — Bootstrap anchors its menu to
     the toggle, and a display:none toggle has nowhere to anchor. So Bulk
     Actions stays in the row, but only once it's usable: while it's disabled
     (nothing selected) it would hold the whole row open for a control you
     can't press. portal.js watches `disabled` and re-runs the collapse. */
  .pp-panel-head .dropdown:has(> .dropdown-toggle:disabled) { display: none; }

  /* Modal → bottom sheet */
  .modal.fade .modal-dialog {
    transform: translateY(100%);
    transition: transform .32s cubic-bezier(.4,0,.2,1);
  }
  .modal.show .modal-dialog { transform: none; }
  .modal-dialog-centered { align-items: flex-end; margin: 0; max-width: 100% !important; min-height: 100%; width: 100%; }
  .pp-modal { border-radius: 20px 20px 0 0 !important; max-height: 90dvh; overflow-y: auto; overscroll-behavior: contain; }
  .pp-modal-bs { border-radius: 20px 20px 0 0 !important; max-height: 90dvh; overflow-y: auto; overscroll-behavior: contain; }
}

/* ── Small tablet (≤768px) ── */
@media (max-width: 768px) {
  .client-item { padding: .6rem 1rem; }
}

/* ── Phone (≤600px) ── */
@media (max-width: 600px) {

  /* ── Back button (client panel) ── */
  .mobile-back-btn {
    display: flex; align-items: center; gap: .35rem;
    background: none; border: none; color: var(--bs-primary);
    font-family: 'DM Sans', sans-serif; font-size: .9rem; font-weight: 600;
    cursor: pointer; padding: .5rem 0 .75rem; width: 100%;
  }


  /* ── Touch targets ── */
  .cal-nav-btn { width: 40px; height: 40px; font-size: .8rem; }
  .cal-today-btn { height: 40px; font-size: .78rem; }
  .cal-btn-sidebar { width: 40px; height: 40px; font-size: .82rem; }
  .client-item { padding: .85rem 1.25rem; }

  /* ── Login card ── */
  .login-card { padding: 2rem 1.5rem; margin: 1rem; border-radius: 16px; }

}

/* ── Dark mode — sidebar needs explicit override to beat Bootstrap offcanvas ── */
[data-bs-theme="dark"] .sidebar { background: var(--pp-sidebar-bg) !important; border-right-color: var(--bs-border-color); }
[data-bs-theme="dark"] .topbar .affiliate-chip a { color: #7dd3fc; }
[data-bs-theme="dark"] .topbar .affiliate-chip a:hover { color: #bae6fd; }
[data-bs-theme="dark"] .affiliate-item.active { background: rgba(220,38,38,.15); }
/* Upcoming-event green text needs a brighter variant in dark mode — #15803d
   on the dark body is only 3.0:1 (fails AA normal text). #4ade80 hits 9.2:1. */
[data-bs-theme="dark"] .upcoming-party-row.status-upcoming .upcoming-date-num,
[data-bs-theme="dark"] .cal-has-events.status-upcoming:not(.cal-today) .cal-day-num {
  color: #4ade80;
}
/* Same green: the row's colored left border + the cal-pip indicator strip
   are UI components and need 3:1. #15803d on dark body is 3.04:1 (passes
   barely); #4ade80 lifts it well clear. */
[data-bs-theme="dark"] .upcoming-party-row.status-upcoming { border-color: #4ade80; }
[data-bs-theme="dark"] .event-table-row.status-upcoming > td:nth-child(2) { border-left-color: #4ade80; }
[data-bs-theme="dark"] .cal-pip.pip-upcoming { background: #4ade80; }

/* WCAG 1.4.11 — Bootstrap's default form-control border #dee2e6 on white
   is 1.36:1 (well under the 3:1 UI minimum). Slate-500: 4.75:1 on white,
   4.43:1 on the Aether page tint #f6f7f9. (The previous slate-400 #94a3b8
   was 2.56:1 on white — the old comment claiming 3.0:1 was wrong, and the
   page tint would have pushed it further under.) Dark mode keeps Bootstrap's
   default — the input bg is already dark so the border has plenty of contrast. */
.form-control, .form-select, .form-check-input {
  border-color: #64748b;
}
[data-bs-theme="dark"] .form-control,
[data-bs-theme="dark"] .form-select,
[data-bs-theme="dark"] .form-check-input {
  border-color: #495057;   /* Bootstrap dark default; 4.5:1 on body bg */
}
/* Status-past row opacity tipped sub-text below AA. Apply opacity only to the
   non-text elements (date column + leading icon) and softly desaturate the
   row's primary text instead of dropping its opacity. */
.fcal-event-row.status-past .fcal-event-date { opacity: .55; }
.fcal-event-row.status-past .fcal-event-title { color: var(--bs-secondary-color); }
.fcal-event-row.status-past .fcal-event-client,
.fcal-event-row.status-past .fcal-event-aff { opacity: .85; }
[data-bs-theme="dark"] .aff-action-btn:hover { background: var(--bs-tertiary-bg); border-color: var(--bs-border-color); color: var(--bs-primary); }
[data-bs-theme="dark"] .aff-action-btn.aff-danger:hover { background: rgba(var(--bs-danger-rgb),.15); border-color: rgba(var(--bs-danger-rgb),.3); color: #f87171; }
[data-bs-theme="dark"] .btn-ann-like.liked { background: rgba(232,121,249,.12); }
[data-bs-theme="dark"] .cal-has-ann:not(.cal-has-events):hover { background: rgba(217,119,6,.12); }
[data-bs-theme="dark"] .affmgr-table thead th { background: var(--bs-tertiary-bg); }
[data-bs-theme="dark"] .affmgr-table tbody td.aff-td-check,
[data-bs-theme="dark"] #myClientsTable tbody td:first-child,
[data-bs-theme="dark"] #allClientsTable tbody td:first-child { background: var(--bs-body-bg); }
[data-bs-theme="dark"] .fc { background: var(--bs-body-bg); color: var(--bs-body-color); }
[data-bs-theme="dark"] #lottieCont,
[data-bs-theme="dark"] #pfLottieCont,
[data-bs-theme="dark"] .od-preview-lottie { filter: invert(1); }
/* Dark mode: use a brighter blue that satisfies dot/UI contrast on the dark
   body AND is light enough that dark text on top hits AA. Accents are
   amber-400 / emerald-400 — bright on the body, and dark text on top hits
   10+:1 either way. */
[data-bs-theme="dark"] {
  --bs-primary: #60a5fa; --bs-primary-rgb: 96,165,250;   /* blue-400 — 5.9:1 on #212529 (UI 3:1 ✓); 6.7:1 vs #0f172a text */
  --pp-accent-order: #fbbf24;                            /* amber-400 — 13.0:1 on #212529 */
  --pp-accent-ann:   #34d399;                            /* emerald-400 — 13.5:1 on #212529 */
  --pp-on-primary:      #0f172a;                         /* dark text on blue-400 — 6.7:1 */
  /* on-accent-order / on-accent-ann stay dark (10+:1) */
}

/* ── Page loader ── */
#pageLoader {
  position: fixed; inset: 0; z-index: 99998;
  background: var(--bs-tertiary-bg);
  display: flex; align-items: center; justify-content: center;
  opacity: 0; pointer-events: none;
  transition: opacity .35s;
}
#pageLoader.visible { opacity: 1; pointer-events: auto; }
#lottieCont { width: 120px; height: 120px; }

/* ── Party Favors tree panel ─────────────────────────────────────────── */
.pf-tree-actions {
  display: flex; gap: .45rem;
  padding: .65rem .9rem;
  border-bottom: 1px solid var(--bs-border-color);
  flex-shrink: 0;
}
.pf-tree-actions .btn { flex: 1; font-size: .78rem; }

.pf-tree { flex: 1; overflow-y: auto; padding: .35rem 0; }

.pf-tree-cat {
  display: flex; align-items: center; gap: .55rem;
  padding: .52rem 1rem; cursor: pointer;
  border-left: 3px solid transparent;
  transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t);
}
.pf-tree-cat:hover  { background: var(--bs-tertiary-bg); border-left-color: var(--bs-primary); }
.pf-tree-cat.active { background: rgba(var(--bs-primary-rgb),.08); border-left-color: var(--bs-primary); }
.pf-tree-cat-icon { color: var(--bs-primary); font-size: .82rem; width: .9rem; text-align: center; flex-shrink: 0; }
.pf-tree-cat-name { font-size: .88rem; font-weight: 600; color: var(--bs-emphasis-color); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pf-tree-count { font-size: .7rem; color: var(--bs-primary); background: rgba(var(--bs-primary-rgb), .12); border-radius: 999px; padding: .05rem .42rem; flex-shrink: 0; }  /* Aether: tinted like the sidebar count badges */

.pf-tree-prod {
  display: flex; align-items: center; gap: .45rem;
  padding: .32rem 1rem .32rem 2.15rem;
  cursor: pointer; transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t);
}
.pf-tree-prod:hover { background: var(--bs-tertiary-bg); }
.pf-tree-prod:hover .pf-tree-prod-name { color: var(--bs-primary); }
.pf-tree-prod-icon { color: var(--bs-border-color); font-size: .72rem; flex-shrink: 0; }
.pf-tree-prod-name { font-size: .8rem; color: var(--bs-secondary-color); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; transition: color .15s; }

/* ── Party Favors main panel ─────────────────────────────────────────── */
.pf-cat-header {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 1rem; margin-bottom: 1rem;
}
.pf-cat-thumb {
  width: 54px; height: 54px; border-radius: 10px;
  background: var(--bs-tertiary-bg); flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden; font-size: 1.5rem; color: var(--bs-primary);
}
.pf-cat-thumb img { width: 100%; height: 100%; object-fit: cover; }
.pf-cat-title { font-size: 1.05rem; font-weight: 700; color: var(--bs-emphasis-color); line-height: 1.3; }
.pf-cat-desc  { font-size: .82rem; color: var(--bs-secondary-color); margin-top: .2rem; line-height: 1.5; }
.pf-cat-count { font-size: .73rem; color: var(--bs-secondary-color); margin-top: .3rem; }

.pf-product-row {
  display: flex; align-items: center; gap: .85rem;
  padding: .65rem 0; border-bottom: 1px solid var(--bs-border-color);
}
.pf-product-row:last-child { border-bottom: none; }
.pf-product-thumb {
  width: 44px; height: 44px; border-radius: 8px;
  background: var(--bs-tertiary-bg); flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden; color: var(--bs-border-color); font-size: 1rem;
}
.pf-product-thumb img { width: 100%; height: 100%; object-fit: cover; }
.pf-product-name { font-size: .9rem; font-weight: 600; color: var(--bs-emphasis-color); }
.pf-product-desc {
  font-size: .78rem; color: var(--bs-secondary-color); margin-top: .1rem; line-height: 1.4;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* ── Shared image picker (category + product modals) ─────────────── */
.catalog-img-picker {
  position: relative; width: 100%; aspect-ratio: 16 / 9;
  border: 2px dashed var(--bs-border-color); border-radius: var(--bs-border-radius-lg);
  overflow: hidden; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  background: var(--bs-tertiary-bg); transition: border-color .2s;
}
.catalog-img-picker:hover { border-color: var(--bs-primary); }
.catalog-img-picker .img-placeholder {
  display: flex; flex-direction: column; align-items: center; gap: .5rem;
  color: var(--bs-secondary-color); font-size: .85rem; text-align: center; pointer-events: none;
}
.catalog-img-picker .img-placeholder i { font-size: 2rem; }

/* ── Affiliates / All Clients manager pages ───────────────────────── */
#adminAllClientsList { flex:1; overflow-y:auto; min-height:0; }
.affmgr-title { font-size:.95rem; font-weight:700; color:var(--bs-emphasis-color); display:flex; align-items:center; gap:.45rem; }
.affmgr-title i { color:var(--bs-primary); }
/* ── Affiliates / All Clients Table Toolbar ───────────────────────── */
.affmgr-header-row { display:flex; align-items:center; justify-content:space-between; padding:1.1rem 1.75rem .9rem; border-bottom:1px solid var(--bs-border-color); flex-shrink:0; gap:.75rem; flex-wrap:wrap; }
.affmgr-header-actions { display:flex; align-items:center; gap:.5rem; }
.affmgr-table-wrap { flex:1; overflow:auto; min-height:0; overscroll-behavior:none; }
.affmgr-table { margin:0; font-size:.84rem; white-space:nowrap; border-collapse:collapse; width:100%; }
/* Once a column has been resized, the table switches to fixed layout so per-column
   widths actually take effect (in the default auto layout the browser ignores them).
   width:max-content lets the table size to the sum of its columns and scroll inside the
   overflow-x wrapper — without it, Bootstrap's .table width:100% renormalizes the columns
   proportionally and a per-column width never sticks. */
/* min-width:100% so a frozen table whose columns sum to LESS than the wrapper still
   fills the page (was leaving big empty space on the right); width:max-content lets it
   grow past 100% and scroll inside the overflow wrapper when columns are wider. */
.affmgr-table.pp-cols-frozen { table-layout:fixed; width:max-content; min-width:100%; }
.affmgr-table thead th[draggable] { cursor:grab; user-select:none; }
.affmgr-table thead th[draggable]:active { cursor:grabbing; }
.affmgr-table thead th.col-dragging { opacity:.35; }
.affmgr-table thead th.col-drag-over { box-shadow:-3px 0 0 0 var(--bs-primary) inset; }
.col-resize-handle { position:absolute; right:0; top:0; bottom:0; width:5px; cursor:col-resize; z-index:3; }
.col-resize-handle:hover, .col-resize-handle.col-resize-active { background:var(--bs-primary); opacity:.45; }
body.col-resizing, body.col-resizing * { cursor:col-resize !important; user-select:none !important; }
.fc-daygrid-day { cursor:pointer; }
.affmgr-table thead th {
  position:sticky; top:0; z-index:2;
  background:var(--bs-tertiary-bg);
  border-top:none !important;
  border-bottom:1px solid var(--bs-border-color) !important;
  font-size:.69rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em;
  color:var(--bs-secondary-color); padding:.75rem 1rem; vertical-align:middle; white-space:nowrap;
}
/* Corner-sticky: both top:0 (from thead th) and left:0 */
.affmgr-th-check { width:40px !important; padding:0 !important; }
.affmgr-table thead th.affmgr-th-check { position:sticky; top:0; left:0; z-index:4; }
.affmgr-table tbody td.aff-td-check { position:sticky; left:0; z-index:1; background:var(--bs-body-bg); box-shadow:inset -1px 0 0 var(--bs-border-color); will-change:transform; padding:0 !important; }
.aff-check-label { display:flex; align-items:center; padding:.72rem .6rem .72rem 1.25rem; cursor:pointer; width:100%; }
/* Sticky Name column on client-only tables (no checkbox column).
   Desktop only — on mobile (<768px) the sticky name eats too much of
   the visible row width, so we let it scroll with the rest of the
   columns. The thead row stays vertically sticky (top:0) via the
   broader thead rule above. */
@media (min-width: 768px) {
  #myClientsTable thead th.affmgr-th-name,
  #allClientsTable thead th.affmgr-th-name { position:sticky; top:0; left:0; z-index:4; }
  #myClientsTable tbody td:first-child,
  #allClientsTable tbody td:first-child { position:sticky; left:0; z-index:1; background:var(--bs-body-bg); box-shadow:inset -1px 0 0 var(--bs-border-color); will-change:transform; overflow:visible; max-width:none; }
}
.affmgr-th-name   { min-width:190px; }
.affmgr-th-company{ min-width:140px; }
.affmgr-th-num    { min-width:82px; }
.affmgr-th-date   { min-width:115px; }
.affmgr-th-status { min-width:95px; }
.affmgr-th-cat    { min-width:130px; }
.affmgr-th-aff    { min-width:150px; }
.affmgr-th-loc    { min-width:120px; }
.affmgr-th-actions{ min-width:110px; text-align:right !important; }
.affmgr-table tbody td {
  padding:.7rem 1rem; vertical-align:middle;
  border-bottom:1px solid var(--bs-border-color);
  max-width:0; overflow:hidden; text-overflow:ellipsis;
  transition:background .1s;
}
.affmgr-table tbody td.aff-td-actions,
.affmgr-table tbody td.aff-td-check { overflow:visible; max-width:none; }
.affmgr-table tbody td.aff-td-actions { text-align:right; padding-right:1.25rem; }
.affmgr-table tbody tr { cursor:pointer; }
.affmgr-table tbody tr:hover td { background: rgba(var(--bs-primary-rgb),.06); }
.affmgr-table tbody tr:hover td.aff-td-check,
#myClientsTable tbody tr:hover td:first-child,
#allClientsTable tbody tr:hover td:first-child { background: var(--bs-tertiary-bg); }
.affmgr-table tbody tr:last-child td { border-bottom:none; }
.sortable { cursor:pointer; user-select:none; }
.sortable:hover { color:var(--bs-emphasis-color); }
.affmgr-sort-icon { font-size:.58rem; margin-left:.25rem; color:var(--bs-border-color); vertical-align:middle; transition:color .15s; }
.sortable:hover .affmgr-sort-icon { color:var(--bs-secondary-color); }
.affmgr-table thead th.sort-active { color:var(--bs-emphasis-color); }
.affmgr-table thead th.sort-active .affmgr-sort-icon { color:var(--bs-primary); }
.status-indicator-online { display:inline-block; width:8px; height:8px; border-radius:50%; background:var(--bs-success); margin-right:5px; vertical-align:middle; flex-shrink:0; }

/* ── Online Now tile ─────────────────────────────────────────────────
   Inherits all chrome (bg/border/radius/shadow/padding) from .stat-chart
   so it sits next to the podium as a peer. Header row uses .stat-chart-hdr
   (label + count). Body is the wrapped name list. A "View all" link sits
   along the bottom edge like a footer. */
.pp-online-tile .pp-online-tile-label-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--bs-success);
  flex-shrink: 0;
  display: inline-block;
}
/* Empty state — nobody online. Dot + label go red so the admin notices
   the dashboard is quiet right now. */
.pp-online-tile--empty #onlineNowLabel { color: var(--bs-danger); }
.pp-online-tile--empty .pp-online-tile-label-dot { background: var(--bs-danger); }
.pp-online-tile--empty .stat-chart-now { display: none; }
.pp-online-tile .pp-online-tile-names {
  /* One affiliate per row (name + track chip + location, all visible) —
     was inline middot-separated names with the details tooltip-only. */
  /* Capped at 5 rows in JS + a "+N more" link — no inner scrollbar (Jeff). */
  flex: 1; display: flex; flex-direction: column;
  align-content: flex-start; gap: .18rem;
  min-width: 0;
  font-size: .82rem;
  margin: 0 0 .25rem;
}
/* Grouped-by-section layout: a header line per portal section, names as
   wrapping chips beneath it. EVERYONE fits — nothing scrolls, nothing is
   truncated (Jeff). A crowd collapses into a few lines instead of N tall rows. */
.pp-online-group { display: flex; flex-direction: column; gap: .22rem; }
.pp-online-group + .pp-online-group { margin-top: .5rem; }
.pp-online-group-hdr {
  display: flex; align-items: center; gap: .35rem;
  font-size: .66rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
  color: var(--bs-secondary-color);
}
.pp-online-group-n {
  font-size: .6rem; font-weight: 700; line-height: 1.4;
  color: var(--bs-secondary-color); background: var(--bs-tertiary-bg);
  border-radius: 999px; padding: 0 .34rem; font-variant-numeric: tabular-nums;
}
.pp-online-chips { display: flex; flex-wrap: wrap; gap: .3rem; }
/* Name on top, track UNDER it (Jeff) — so the chip reads as one person's
   identity block rather than two competing pills side by side. */
.pp-online-chip {
  display: inline-flex; flex-direction: column; align-items: flex-start; gap: .05rem;
  padding: .22rem .6rem; max-width: 100%; text-align: left;
  border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius);
  background: var(--bs-body-bg); color: var(--bs-emphasis-color);
  font-size: .76rem; font-weight: 600; line-height: 1.25; cursor: pointer;
  transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t);
}
.pp-online-chip:hover { border-color: var(--bs-primary); color: var(--bs-primary); }
.pp-online-chip-track {
  font-size: .58rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  color: var(--bs-secondary-color); line-height: 1.3;
}
/* The online tile IS a dashboard drawer now (same markup + mechanics as Quick
   stats), so it inherits .admin-dash-drawer chrome. Only the count needs a
   size bump in the header row — it sits right-aligned via ms-auto. */
.pp-online-tile .stat-chart-now { font-size: .95rem; }
.pp-online-tile-name {
  background: none; border: 0; padding: 0;
  font-size: inherit; font-weight: 600;
  color: var(--bs-emphasis-color);
  cursor: pointer;
  transition: color .12s;
}
.pp-online-tile-name:hover { color: var(--bs-primary); text-decoration: underline; }
.pp-online-tile-action {
  background: none; border: 0; padding: 0;
  font-size: .67rem; color: var(--bs-primary); font-weight: 600;
  display: inline-flex; align-items: center; gap: .3rem;
  cursor: pointer; align-self: flex-end;
  margin-top: auto;
}
.pp-online-tile-action:hover { text-decoration: underline; }
/* Bottom row of the admin dashboard: podium (Top 3 affiliates) and the
   Online Now tile, side-by-side. Stacks on narrow screens so the podium
   keeps room to breathe. */
.pp-dash-bottom {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr);
  gap: .5rem;
  margin-top: .5rem;
  align-items: stretch;
}
.pp-dash-bottom-podium,
.pp-dash-bottom > .pp-online-tile { min-width: 0; }
.pp-dash-bottom-podium .stat-chart,
.pp-dash-bottom > .pp-online-tile { height: 100%; }
@media (max-width: 720px) {
  .pp-dash-bottom { grid-template-columns: 1fr; }
}
.aff-name-primary { font-weight:600; font-size:.875rem; color:var(--bs-emphasis-color); line-height:1.3; overflow:hidden; text-overflow:ellipsis; max-width:200px; }
.aff-name-secondary { font-size:.75rem; color:var(--bs-secondary-color); margin-top:1px; overflow:hidden; text-overflow:ellipsis; max-width:200px; }
.aff-action-btn {
  background:none; border:1px solid transparent;
  padding:.3rem .45rem; border-radius:var(--bs-border-radius-sm);
  cursor:pointer; font-size:.78rem; color:var(--bs-secondary-color);
  transition:background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t); line-height:1; vertical-align:middle;
}
.aff-action-btn:hover { background:var(--bs-body-bg); border-color:var(--bs-border-color); color:var(--bs-primary); box-shadow:0 1px 2px rgba(0,0,0,.06); }
.aff-action-btn.aff-danger:hover { color:var(--bs-danger); background:rgba(var(--bs-danger-rgb),.08); border-color:rgba(var(--bs-danger-rgb),.3); }
.affmgr-table .aff-table-row.active td,
.affmgr-table .all-client-table-row.active td,
.affmgr-table .my-client-table-row.active td { background: rgba(var(--bs-primary-rgb),.08) !important; }
.affmgr-table .aff-table-row.active td.aff-td-check,
.affmgr-table .all-client-table-row.active td.aff-td-check,
.affmgr-table .my-client-table-row.active td.aff-td-check,
#myClientsTable .my-client-table-row.active td:first-child,
#allClientsTable .all-client-table-row.active td:first-child { background: var(--bs-secondary-bg) !important; }
.affmgr-table .aff-table-row.active td:first-child,
.affmgr-table .all-client-table-row.active td:first-child,
.affmgr-table .my-client-table-row.active td:first-child { box-shadow:inset 3px 0 0 var(--bs-primary); }
.affmgr-table .aff-table-row.table-active td { background: rgba(var(--bs-primary-rgb),.08); }
.affmgr-empty { display:flex; flex-direction:column; align-items:center; justify-content:center; padding:4rem 2rem; color:var(--bs-secondary-color); text-align:center; flex:1; }
.affmgr-empty i { font-size:2.5rem; opacity:.15; margin-bottom:.85rem; display:block; }
.affmgr-empty .btn { margin-top: .85rem; }
.affiliate-item {
  padding:.875rem 1.75rem; cursor:pointer;
  border-bottom:1px solid var(--bs-border-color);
  display:flex; align-items:center; justify-content:space-between; gap:1rem;
  transition:background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t);
}
.affiliate-item:hover { background:var(--bs-tertiary-bg); }
.affiliate-item.active { background: rgba(var(--bs-primary-rgb),.08); }
.affiliate-item-main { min-width:0; flex:1; }
.affiliate-item-name { font-weight:600; font-size:.95rem; color:var(--bs-emphasis-color); }
.affiliate-item-sub { font-size:.82rem; color:var(--bs-secondary-color); margin-top:2px; }
.affiliate-item-meta { display:flex; align-items:center; gap:.65rem; flex-shrink:0; }
.affiliate-item-stat { font-size:.78rem; color:var(--bs-secondary-color); display:flex; align-items:center; gap:.25rem; }
.all-client-item {
  padding:.75rem 1.75rem; cursor:pointer;
  border-bottom:1px solid var(--bs-border-color);
  display:flex; align-items:center; justify-content:space-between; gap:1rem;
  transition:background .15s;
}
.all-client-item:hover { background:var(--bs-tertiary-bg); }
.all-client-item.active { background: rgba(var(--bs-primary-rgb),.08); }

/* ── Theme / Design Manager ──────────────────────────────────────── */
.theme-mgr-header {
  padding: 1rem 1.5rem .875rem;
  border-bottom: 1px solid var(--bs-border-color);
  flex-shrink: 0;
}
.theme-mgr-title { font-size: .95rem; font-weight: 700; color: var(--bs-emphasis-color); display: flex; align-items: center; gap: .45rem; }
.theme-mgr-title i { color: var(--bs-primary); }
.theme-mgr-subtitle { font-size: .78rem; color: var(--bs-secondary-color); margin-top: .2rem; }
.theme-preview-label {
  font-size: .7rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--bs-secondary-color); font-weight: 700; margin-bottom: 1rem;
}
.theme-preview-inner { max-width: 380px; }
.theme-section {
  margin-bottom: 1.25rem; padding-bottom: 1.25rem;
  border-bottom: 1px solid var(--bs-border-color);
}
.theme-section:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
.theme-section-title {
  font-size: .7rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--bs-secondary-color); font-weight: 700; margin-bottom: .8rem;
  display: flex; align-items: center; gap: .35rem;
}
.theme-row { margin-bottom: .7rem; }
.theme-row:last-child { margin-bottom: 0; }
.theme-row-label {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: .82rem; margin-bottom: .2rem; color: var(--bs-body-color);
}
.theme-val { font-size: .72rem; color: var(--bs-secondary-color); font-family: 'Courier New', monospace; }
.shadow-presets { display: flex; gap: .4rem; }
.shadow-preset-btn {
  flex: 1; padding: .3rem .4rem;
  border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius);
  background: var(--bs-body-bg); color: var(--bs-body-color);
  font-size: .75rem; cursor: pointer; transition: border-color .15s, color .15s, background .15s;
  font-family: 'DM Sans', sans-serif;
}
.shadow-preset-btn:hover { border-color: var(--bs-primary); color: var(--bs-primary); }
.shadow-preset-btn.active { border-color: var(--bs-primary); color: var(--bs-primary); background: rgba(var(--bs-primary-rgb),.12); }
.theme-mgr-footer {
  padding: .75rem 1.1rem; border-top: 1px solid var(--bs-border-color);
  display: flex; justify-content: space-between; align-items: center;
  background: var(--bs-body-bg); flex-shrink: 0;
}


/* ── Button helpers ── */
.btn-pp {
  border: none; border-radius: var(--bs-border-radius); padding: .65rem 1.25rem;
  font-size: .9rem; font-family: 'DM Sans', sans-serif; font-weight: 600;
  cursor: pointer; transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t);
}
.btn-pp:disabled { opacity: .6; cursor: not-allowed; }
.btn-icon { width: 32px; height: 32px; padding: 0 !important; flex-shrink: 0; }
.btn-icon-lg { width: 36px; height: 36px; }
.btn-primary .spinner-border, .btn-submit .spinner-border { border-color: rgba(255,255,255,.4); border-top-color: #fff; }
.spinner-border-sm { width: .875rem; height: .875rem; border-width: .15em; vertical-align: middle; margin-right: .3rem; }

/* ── Design page scroll fix (all sizes) ── */
#themeManager > .row { min-height: 0; }
#themeManager > .row > div { min-height: 0; height: 100%; }

/* ── Dashboard: sticky calendar sidebar ── */
@media (min-width: 992px) {
  #welcomeState { align-items: flex-start; }
  #dashCalSidebar {
    position: sticky;
    top: 0;
    /* Desktop has no top bar anymore (shell rebuild) — full viewport height */
    height: 100vh;
    height: 100dvh;
    overflow-y: auto;
  }
}

/* ── Place Card Designer ─────────────────────────────────────────── */











/* ── Snap-to-guides pill (floating inside canvas wrap) ── */




/* ── Tour info button — small info icon in bottom-right corner of canvas ── */


/* ── Portrait / Landscape layout toggle buttons ── */











/* Default: resolve un-suffixed --pcd-* vars from the portrait set.
   Overridden below by @container (orientation: landscape). */







/* Wrapper around the banner img — receives the 3D flip animation so the
   image's own scale/translate transform (rewritten on every preview update)
   never collides with our rotation. */

/* Half-flip + return: rotates to edge-on at 50%, then back to identity.
   pcdBannerFlip swaps the actual scaleX/scaleY mirror at the midpoint while
   the wrapper is invisible — the user perceives one continuous card flip. */


/* Snappy timing: ~200ms total, cubic-bezier(.2,0,0,1) — fast off the start
   with a sharp deceleration so the flip feels crisp, not floaty. */












  /* DATE ——— */
  /* ——— DATE */









#mainPanel { overflow-x: clip; } /* clip (not hidden) avoids iOS dual-axis scroll container bug */


/* ── Mobile: search bars & design page ── */
@media (max-width: 767.98px) {
#themeManager > .row { align-content: flex-start !important; overflow-y: auto !important; overflow-x: hidden !important; }
  #themeManager > .row > div { overflow: visible !important; height: auto !important; }
}


/* ── PDF status pill pulse (for "Generating…") ── */
@keyframes pdf-pill-pulse { 0%, 100% { opacity: .85; } 50% { opacity: 1; } }

/* ── Full-page calendar (FullCalendar) theme bridge ── */
#fullCalGrid {
  --fc-border-color:               var(--bs-border-color-translucent);  /* lighter grid */
  --fc-page-bg-color:              var(--bs-body-bg);
  --fc-neutral-bg-color:           var(--bs-tertiary-bg);
  --fc-neutral-text-color:         var(--bs-secondary-color);
  --fc-list-event-hover-bg-color:  var(--bs-tertiary-bg);
  --fc-today-bg-color:             transparent;  /* bold day number + dot is the indicator */
  --fc-event-bg-color:             var(--bs-primary);
  --fc-event-border-color:         var(--bs-primary);
  --fc-event-text-color:           var(--bs-body-color);  /* chips use body text now */
  color: var(--bs-body-color);
}
#fullCalGrid .fc-toolbar-title {
  font-size: 1.2rem;
  font-weight: 600;
  letter-spacing: -.015em;
  color: var(--bs-emphasis-color);
}

/* Toolbar buttons: ghost style for navigation/inactive views, filled for
   the active view. Reads as a refined segmented control instead of a row
   of identically loud primary CTAs. */
#fullCalGrid .fc-button {
  background: transparent;
  border: 1px solid var(--bs-border-color);
  color: var(--bs-body-color);
  font-size: .78rem;
  font-weight: 500;
  padding: .3rem .65rem;
  box-shadow: none !important;
  transition: background-color .12s, border-color .12s, color .12s;
}
#fullCalGrid .fc-button:not(:disabled):hover {
  background: var(--bs-tertiary-bg);
  border-color: var(--bs-border-color);
  color: var(--bs-body-color);
}
#fullCalGrid .fc-button:not(:disabled):focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: 1px;
}
#fullCalGrid .fc-button-active,
#fullCalGrid .fc-button-active:hover,
#fullCalGrid .fc-button-primary:not(:disabled).fc-button-active {
  background: var(--bs-primary);
  border-color: var(--bs-primary);
  color: var(--pp-on-primary, #fff);
}
#fullCalGrid .fc-button:disabled {
  opacity: .45;
  cursor: not-allowed;
}
/* Tighten the button groups so the toolbar reads as fewer, calmer chunks */
#fullCalGrid .fc-button-group .fc-button {
  border-radius: 0;
}
#fullCalGrid .fc-button-group .fc-button:first-child { border-top-left-radius: .375rem; border-bottom-left-radius: .375rem; }
#fullCalGrid .fc-button-group .fc-button:last-child  { border-top-right-radius: .375rem; border-bottom-right-radius: .375rem; }
#fullCalGrid .fc-button-group .fc-button + .fc-button { margin-left: -1px; }
#fullCalGrid .fc-daygrid-day-number {
  color: var(--bs-body-color);
  text-decoration: none;
  padding: 6px 8px 2px;
  font-weight: 500;
  font-size: .95rem;
  font-variant-numeric: tabular-nums;
}
#fullCalGrid .fc-col-header-cell-cushion {
  color: var(--bs-body-color);
  text-decoration: none;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .08em;
  font-size: .75rem;
  padding: 10px 0;
}

/* Day-cell hover: a faint warm-up that hints at clickability. Transition is
   short so the calendar doesn't feel "fuzzy" as the cursor passes over. */
.fc .fc-daygrid-day { transition: background-color .12s ease; }
.fc .fc-daygrid-day:not(.fc-day-today):hover { background-color: var(--bs-tertiary-bg); }
/* Dark mode: --bs-tertiary-bg sits just 4% off the body bg, which is hard
   to perceive as a hover state on the calendar grid. Brighten it. */
[data-bs-theme="dark"] .fc .fc-daygrid-day:not(.fc-day-today):hover {
  background-color: rgba(255, 255, 255, .055);
}

/* Today: bold primary-colored number + a tiny accent dot underneath. No
   filled badge — reads as natural typography emphasis instead of a chip,
   which feels heavy at small calendar scales. */
.fc .fc-day-today { background: transparent !important; }
.fc .fc-day-today .fc-daygrid-day-number {
  color: var(--bs-primary) !important;
  font-weight: 700;
  position: relative;
  padding-bottom: 7px !important;
}
.fc .fc-day-today .fc-daygrid-day-number::after {
  content: '';
  position: absolute;
  left: 50%; bottom: 2px;
  transform: translateX(-50%);
  width: 4px; height: 4px;
  border-radius: 50%;
  background: var(--bs-primary);
}
.fcal-widget-card .fc .fc-day-today .fc-daygrid-day-number {
  padding-bottom: 6px !important;
}
.fcal-widget-card .fc .fc-day-today .fc-daygrid-day-number::after {
  width: 3px; height: 3px;
}

/* Past dates: subtle dim so the eye is drawn to upcoming/today/future.
   Apply opacity to the day NUMBER only; for event chips dim the text but
   keep the colored left bar at full opacity (WCAG 1.4.11 requires 3:1
   for the UI indicator, which the bar provides). */
.fc .fc-day-past .fc-daygrid-day-number { opacity: .55; }
.fc .fc-day-past .fcal-ev .fc-event-main { opacity: .6; }

/* Weekend cells: faint zebra so the week structure reads without a hard rule.
   Static rgba (not Bootstrap vars) because --bs-tertiary-bg already has a
   visible alpha that would stack too dark with cell hover. */
.fc .fc-day-sat, .fc .fc-day-sun { background-color: rgba(0,0,0,.025); }
[data-bs-theme="dark"] .fc .fc-day-sat,
[data-bs-theme="dark"] .fc .fc-day-sun { background-color: rgba(255,255,255,.03); }
#fullCalGrid a,
#fullCalGrid a:hover,
.fcal-widget-card a,
.fcal-widget-card a:hover { text-decoration: none !important; }
/* FC injects a[data-navlink]:hover { text-decoration:underline } — squash it */
#fullCalGrid [data-navlink],
#fullCalGrid [data-navlink]:hover,
.fcal-widget-card [data-navlink],
.fcal-widget-card [data-navlink]:hover { text-decoration: none !important; }
#fullCalGrid .fc-daygrid-more-link {
  color: var(--bs-secondary-color);
  font-size: .68rem;
  font-weight: 500;
  padding: 2px 4px 2px 8px;
  background: transparent;
  border-radius: 3px;
  transition: color .12s;
}
#fullCalGrid .fc-daygrid-more-link:hover {
  color: var(--bs-primary);
  background: transparent;
  text-decoration: none !important;
}
.fc-popover {
  background: var(--bs-body-bg) !important;
  color: var(--bs-body-color) !important;
  border: 1px solid var(--bs-border-color) !important;
  border-radius: var(--bs-border-radius) !important;
  box-shadow: 0 12px 32px rgba(0,0,0,.18) !important;
  overflow: hidden;
  min-width: 260px !important;
  max-width: 360px;
  font-size: 1rem;
}
.fc-popover-header {
  background: var(--bs-tertiary-bg) !important;
  color: var(--bs-body-color) !important;
  padding: .65rem .9rem !important;
  font-weight: 600;
  font-size: .9rem;
  display: flex !important;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
}
.fc-popover-close { font-size: 1.1rem !important; opacity: .7; }
.fc-popover-close:hover { opacity: 1; }
.fc-popover-body { padding: .65rem .8rem .8rem !important; display: flex; flex-direction: column; gap: 5px; }
/* Event chips inside the popover get more breathing room — the popover is
   where overflow events go, so they need to read as a proper list. */
.fc-popover-body .fcal-ev {
  font-size: .85rem !important;
  padding: 5px 9px 5px 10px !important;
  border-radius: 5px !important;
  line-height: 1.4;
}

/* Event block styling — party vs order.
   FC injects .fc-h-event { background-color: var(--fc-event-bg-color); border: 1px solid ... }
   via a <style> tag after portal.css. Pill-shaped chips with a soft shadow,
   no harsh accent border. Solid backgrounds (no alpha) read better at chip
   sizes than the previous 55% alpha rendering. */
/* Modern event chip: subtle accent-tinted bg + colored left bar + body text.
   The colored bar (3px) tells you the type; the text reads normally because
   it's body-color on a near-transparent tint. Solves the legibility problem
   (white-on-yellow popover pills) by removing the filled-color pill entirely.
   color-mix() universally supported (Chrome 111+, FF 113+, Safari 16.2+). */
.fcal-ev {
  font-size: .8rem;
  padding: 3px 7px 3px 8px;
  border-radius: 4px;
  border: none !important;
  border-left: 3px solid var(--fc-event-bg-color) !important;
  background: color-mix(in srgb, var(--fc-event-bg-color) 14%, transparent) !important;
  color: var(--bs-body-color) !important;
  font-weight: 500;
  line-height: 1.45;
  overflow: hidden;
  box-shadow: none !important;
  cursor: pointer;
  transition: background-color .12s;
}
.fcal-ev:hover {
  background: color-mix(in srgb, var(--fc-event-bg-color) 24%, transparent) !important;
  transform: none !important;
}
.fcal-ev:focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: 1px;
}
.fcal-ev .fc-event-main {
  color: var(--bs-body-color) !important;
  display: flex; align-items: center; gap: 5px;
  overflow: hidden;
}
.fcal-ev-party { --fc-event-bg-color: var(--pp-accent-party, var(--bs-primary)); }
.fcal-ev-order { --fc-event-bg-color: var(--pp-accent-order, #d97706); }
.fcal-ev-ann   { --fc-event-bg-color: var(--pp-accent-ann, var(--bs-success, #198754)); }
/* Type-icon inside the chip: small + colored to match the bar, so the chip
   reads at a glance even in monochrome scans. */
.fcal-ev-party .fcal-ev-icon { color: var(--pp-accent-party, var(--bs-primary)); }
.fcal-ev-order .fcal-ev-icon { color: var(--pp-accent-order, #d97706); }
.fcal-ev-ann   .fcal-ev-icon { color: var(--pp-accent-ann, var(--bs-success, #198754)); }
/* Holidays. Calendar CHROME, not content: they sit under the date number in
   the cell's own space, never in the event stack, so they cannot push a real
   party into "+2 more". Muted by default; the ones people actually throw a
   party for get the party accent, because those are the ones worth acting on.
   Placed in the DOM right after the date number (see _fcalHolidayCell) so it
   reads as part of the date, with the event chips below it. */
.fcal-holiday {
  font-size: .62rem;
  line-height: 1.15;
  padding: 0 5px 3px;
  margin-top: -2px;
  color: var(--bs-secondary-color, #6c757d);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  pointer-events: auto;            /* so the title tooltip works */
}
.fcal-holiday-party {
  color: var(--pp-accent-party, var(--bs-primary));
  font-weight: 600;
}
/* The compact dashboard/sidebar calendars are too small for a name — the day
   keeps a dot instead, so the pattern of "something is on that day" survives
   without crowding a cell that is barely wider than its number. */
.fcal-widget-card .fcal-holiday {
  font-size: 0; padding: 0; height: 5px; text-align: center;
}
.fcal-widget-card .fcal-holiday::before {
  content: '•'; font-size: .6rem; line-height: 5px;
  color: var(--bs-secondary-color, #6c757d);
}
.fcal-widget-card .fcal-holiday-party::before { color: var(--pp-accent-party, var(--bs-primary)); }

.fcal-ev-icon  { font-size: .68rem; flex-shrink: 0; opacity: .9; }
.fcal-ev-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }

/* Grid → sidebar cross-highlight: when an event in the grid is hovered, the
   matching row in the sidebar event list also lights up (and vice versa, via
   the .fcal-event-row:hover rule + paired class on the chip below). */
.fcal-event-row.fcal-event-row-linked,
.fcal-event-row.fcal-event-row-linked:hover {
  background: rgba(var(--bs-primary-rgb), .12);
  outline: 2px solid var(--bs-primary);
  outline-offset: -2px;
}
/* Cross-highlight for chips — subtle tint deepens + a thin ring. Keeps the
   colored left bar as the type indicator (not a transform/scale, since
   text chips don't benefit from the pop the way dots did). */
.fcal-ev.fcal-ev-linked {
  background: color-mix(in srgb, var(--fc-event-bg-color) 26%, transparent) !important;
  box-shadow: inset 0 0 0 1px var(--fc-event-bg-color) !important;
  transform: none !important;
  filter: none !important;
}

/* listMonth view — icon colors need to show on the dot column */
#fullCalGrid .fc-list-event.fcal-ev-party td.fc-list-event-graphic .fc-list-event-dot { border-color: var(--bs-primary); }
#fullCalGrid .fc-list-event.fcal-ev-order td.fc-list-event-graphic .fc-list-event-dot { border-color: var(--pp-accent-order, #d97706); }
#fullCalGrid .fc-list-event.fcal-ev-ann   td.fc-list-event-graphic .fc-list-event-dot { border-color: var(--pp-accent-ann, var(--bs-success, #198754)); }
#fullCalGrid .fc-list-event.fcal-ev-party,
#fullCalGrid .fc-list-event.fcal-ev-order,
#fullCalGrid .fc-list-event.fcal-ev-ann   { background: transparent; color: var(--bs-body-color); }
#fullCalGrid .fc-list-event.fcal-ev-party:hover,
#fullCalGrid .fc-list-event.fcal-ev-order:hover,
#fullCalGrid .fc-list-event.fcal-ev-ann:hover { background: var(--bs-tertiary-bg); }
#fullCalGrid .fc-list-event.fcal-ev-party .fc-list-event-title,
#fullCalGrid .fc-list-event.fcal-ev-order .fc-list-event-title,
#fullCalGrid .fc-list-event.fcal-ev-ann   .fc-list-event-title { color: var(--bs-body-color); }

/* Loading overlay */
.fcal-loading-overlay {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(var(--bs-body-bg-rgb), 0.6);
  z-index: 5;
  border-radius: .5rem;
}

/* Event list row icons */
.fcal-row-icon { margin-right: .35rem; font-size: .78rem; }
.fcal-row-icon-party { color: var(--bs-primary); }
.fcal-row-icon-order { color: var(--pp-accent-order, #d97706); }
.fcal-row-icon-ann   { color: var(--pp-accent-ann, var(--bs-success, #198754)); }

/* ── Compact FullCalendar widget (dashboard + context sidebars) ── */
.fcal-widget-card #calGrid,
.fcal-widget-card #ctxCalGrid,
.fcal-widget-card #meCalGrid {
  --fc-border-color:        var(--bs-border-color-translucent);  /* lighter grid */
  --fc-page-bg-color:       var(--bs-body-bg);
  --fc-neutral-bg-color:    var(--bs-tertiary-bg);
  --fc-today-bg-color:      transparent;
  --fc-button-bg-color:     var(--bs-primary);
  --fc-button-border-color: var(--bs-primary);
  --fc-button-active-bg-color: var(--bs-primary);
  --fc-button-hover-bg-color:  var(--bs-primary);
  color: var(--bs-body-color);
  font-size: .78rem;
}
.fcal-widget-card .fc .fc-toolbar.fc-header-toolbar { margin-bottom: .4rem; }
.fcal-widget-card .fc .fc-toolbar-title { font-size: .85rem; font-weight: 600; letter-spacing: -.01em; }
.fcal-widget-card .fc .fc-button { padding: .12rem .4rem; font-size: .68rem; }
.fcal-widget-card .fc .fc-col-header-cell-cushion {
  padding: 6px 0;
  font-size: .7rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  font-weight: 600;
  color: var(--bs-body-color);
}
.fcal-widget-card .fc .fc-daygrid-day-number {
  padding: 4px 5px 1px;
  font-size: .82rem;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}
/* Widget chips: same colored-bar language as the full cal but smaller bar,
   tighter padding. Title still shows (truncated by FC's overflow). */
.fcal-widget-card .fcal-ev {
  font-size: .58rem;
  padding: 0 4px 0 4px;
  border-left-width: 2px !important;
  border-radius: 2px;
  line-height: 1.4;
}
.fcal-widget-card .fcal-ev-icon { display: none; }      /* no room for an icon at this scale */
.fcal-widget-card .fc-daygrid-more-link {
  font-size: .58rem;
  padding: 0 4px;
  color: var(--bs-secondary-color);
}

/* Mobile: hide the grid-view toggle (listMonth is auto on small screens) +
   bigger touch targets on the nav buttons so prev/next aren't a pixel-hunt. */
@media (max-width: 767.98px) {
  #fullCalGrid .fc-dayGridMonth-button { display: none; }
  #fullCalGrid .fc-toolbar.fc-header-toolbar { flex-wrap: wrap; gap: .4rem; }
  #fullCalGrid .fc-toolbar-chunk { flex: 0 0 auto; }
  #fullCalGrid .fc-button { min-height: 36px; min-width: 36px; padding: .4rem .65rem; }
  #fullCalGrid .fc-daygrid-day-number { padding: 6px 8px; }
  /* listMonth sticky day headers: FC doesn't ship an explicit z-index, so
     events can stack above them on iOS Safari when scrolling. Pin them
     above the rows with a solid background so events scroll under them. */
  #fullCalGrid .fc-list-day,
  #fullCalGrid .fc-list-day-cushion {
    position: sticky;
    top: 0;
    z-index: 5;
    background: var(--bs-tertiary-bg, var(--bs-body-bg));
  }
  #fullCalGrid .fc-list-event,
  #fullCalGrid .fc-list-event-graphic,
  #fullCalGrid .fc-list-event-time,
  #fullCalGrid .fc-list-event-title {
    position: relative;
    z-index: 1;
  }
}

/* Calendar legend — small color key shown above the event list sidebar so
   the chip colors are decoded without the user having to guess. */
.fcal-legend {
  display: flex; flex-wrap: wrap; gap: .75rem;
  font-size: .68rem; line-height: 1;
  color: var(--bs-secondary-color);
  padding: .25rem 0 .5rem;
  border-bottom: 1px solid var(--bs-border-color-translucent);
  margin-bottom: .65rem;
}
.fcal-legend-dot {
  display: inline-block;
  width: .55rem; height: .55rem;
  border-radius: 999px;
  margin-right: .3rem;
  vertical-align: -1px;
}

/* ── Full-page calendar: viewport-height layout ──
   Jeff wants the event list to scroll WITHIN the viewport, not run off the
   bottom of the page — and not to add a second scrollbar next to the panel's.
   So the whole page becomes exactly one viewport tall with no page scroll:
   the calendar column and the event list are each their own (only-when-needed)
   scroller. ── */
@media (min-width: 992px) {
  #fullCalendarManager { height: 100vh; height: 100dvh; overflow: hidden; }
  #fullCalendarManager > article { height: 100%; overflow-y: auto; }
  .fcal-events-aside { height: 100%; overflow-y: auto; }
}

/* ── Full-page calendar event list ── */
.fcal-event-row {
  display: flex; gap: .65rem; align-items: center;
  padding: .55rem .5rem; border-radius: var(--bs-border-radius-sm); cursor: pointer;
  /* Aether: no per-row divider — hover fill + row radius carry separation */
  transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t);
}
.fcal-event-row:hover { background: var(--bs-tertiary-bg); }
[data-bs-theme="dark"] .fcal-event-row:hover { background: rgba(255, 255, 255, .055); }
/* status-past dimming is applied to non-text children only (date, icon)
   plus a desaturated title color. See the per-child rules below the
   dark-mode override block. */
.fcal-event-date {
  flex-shrink: 0; min-width: 44px; text-align: center;
  padding-right: .5rem;
}
.fcal-event-dow { font-size: .62rem; font-weight: 700; letter-spacing: .08em; color: var(--bs-secondary-color); text-transform: uppercase; }
.fcal-event-num { font-size: 1.1rem; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
.fcal-event-info { flex: 1; min-width: 0; }
.fcal-event-title { font-weight: 600; font-size: .88rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.3; }
.fcal-event-client { font-size: .76rem; color: var(--bs-secondary-color); }
.fcal-event-aff { font-size: .7rem; color: var(--bs-secondary-color); }
.fcal-event-row.status-today .fcal-event-num { color: var(--bs-primary); }
.fcal-event-row.status-upcoming .fcal-event-num { color: var(--bs-emphasis-color); }

/* ── Place-card Preview (3D-tilt WebP snapshot of the designed card) ─── */
.pcp-perspective { perspective: 900px; display: flex; justify-content: center; padding: .5rem 0; }
.pcp-tilt        { will-change: transform; transform-style: preserve-3d; transition: box-shadow 120ms linear; }
.pcp-snapshot    { display: block; max-width: 100%; height: auto; border-radius: var(--bs-border-radius); box-shadow: 0 6px 18px rgba(0,0,0,.15); background: #f3f4f6; }
.pcp-snapshot.pcp-landscape { width: 380px; max-height: 260px; }
.pcp-snapshot.pcp-portrait  { width: 260px; max-height: 380px; }
@media (max-width: 480px) {
  .pcp-snapshot.pcp-landscape { width: 100%; max-width: 360px; }
  .pcp-snapshot.pcp-portrait  { width: 100%; max-width: 240px; }
}
/* Table tent: both printed faces side by side, each at its true 2:3 portrait aspect + labeled. */
.pcp-tent-faces { display: flex; gap: 18px; flex-wrap: wrap; justify-content: center; padding: .5rem 0; }
.pcp-tent-face  { display: flex; flex-direction: column; align-items: center; gap: 6px; margin: 0; }
.pcp-tent-img   { display: block; width: 200px; max-width: 42vw; height: auto; border-radius: var(--bs-border-radius);
                  box-shadow: 0 6px 18px rgba(0,0,0,.15); background: #f3f4f6; }
.pcp-tent-face figcaption { font-size: .8rem; font-weight: 600; color: var(--bs-secondary-color); }

/* ── Theme toggle (Light | Dark segmented) ──────────────────────────────
   Replaces an 85-line morphing sun/moon switch (Jeff: "looks very bad").
   Same markup + class names in the Studio (creator-app/src/app.css) so the
   two apps' controls are identical. Reads as a normal segmented control:
   the active side gets the primary tint, the other is a quiet icon. ── */
.pp-theme-toggle {
  display: inline-flex; align-items: center; flex-shrink: 0;
  width: 46px; height: 24px; padding: 2px;
  border: var(--bs-border-width) solid var(--bs-border-color);
  border-radius: 999px; cursor: pointer;
  background: var(--bs-tertiary-bg);
  transition: background-color var(--pp-t), border-color var(--pp-t);
}
.pp-theme-toggle:hover { border-color: var(--bs-primary); }
.pp-theme-knob {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--bs-body-bg); color: var(--bs-primary);
  box-shadow: var(--bs-box-shadow-sm);
  font-size: .6rem; line-height: 1;
  transition: transform .2s ease;
}
/* State is read straight off the theme attribute — no JS class juggling. */
.pp-theme-knob .fa-moon { display: none; }
[data-bs-theme="dark"] .pp-theme-knob { transform: translateX(22px); }
[data-bs-theme="dark"] .pp-theme-knob .fa-sun { display: none; }
[data-bs-theme="dark"] .pp-theme-knob .fa-moon { display: inline; }

/* ── Mobile calendar sidebar panel ────────────────────────────────────────
   On mobile (<lg) the calendar aside is hidden by default and slides in
   from the right when the header toggle button is tapped.
   On desktop (≥992px) layout is unchanged — aside is in normal col flow. ── */
@media (max-width: 991.98px) {
  #dashCalSidebar,
  #ctxCalSidebar {
    position: fixed;
    right: 0;
    top: 48px;   /* .pp-mobilebar height (was the old 64px header) */
    bottom: 0;
    width: min(340px, 88%);
    overflow-y: auto;
    z-index: 1043;
    transform: translateX(110%);
    transition: transform .3s cubic-bezier(.4,0,.2,1);
    background: var(--bs-body-bg);
    border-left: 1px solid var(--bs-border-color);
    box-shadow: -4px 0 24px rgba(0,0,0,.12);
    /* Override Bootstrap col-12 row flow */
    display: block !important;
    flex: none !important;
    max-width: none !important;
    padding: 1rem !important;
  }
  #dashCalSidebar.cal-sidebar-open,
  #ctxCalSidebar.cal-sidebar-open {
    transform: translateX(0);
  }
  .cal-sidebar-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 1042;
    background: rgba(0,0,0,.35);
  }
  .cal-sidebar-backdrop.show {
    display: block;
  }
  /* Active state for the toggle button */
  #calToggleBtn.active {
    background: var(--bs-primary);
    border-color: var(--bs-primary);
    color: var(--pp-on-primary, #fff);
  }
}

/* ── Smooth light/dark theme transitions ──────────────────────────────────
   Applies only during the brief window when the theme attribute is changing.
   JS sets data-theme-transitioning *before* changing data-bs-theme and calls
   getComputedStyle() to flush, so every element's background "from" value is
   recorded before the theme changes — keeping surfaces in sync.
   Color (text) is intentionally NOT transitioned: text going dark→light while
   backgrounds go light→dark produces an illegible midpoint crossing. Text snaps
   instantly; only surfaces and borders fade. ── */
html[data-theme-transitioning] *,
html[data-theme-transitioning] *::before,
html[data-theme-transitioning] *::after {
  transition:
    background-color .3s ease,
    border-color     .3s ease,
    box-shadow       .3s ease !important;
}



/* ── Shipping tracking timeline ─────────────────────────────────── */
.track-list {
  list-style: none;
  padding: 0;
  margin: 0;
  position: relative;
}
.track-list::before {
  content: '';
  position: absolute;
  left: 0.85rem;
  top: 0.3rem;
  bottom: 0.3rem;
  width: 2px;
  background: var(--bs-border-color);
}
.track-event {
  position: relative;
  padding: 0.4rem 0 0.4rem 2.25rem;
  min-height: 2.2rem;
}
.track-event-dot {
  position: absolute;
  left: 0;
  top: 0.45rem;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  background: var(--bs-body-bg);
  border: 2px solid var(--bs-border-color);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.8rem;
  color: var(--bs-secondary-color);
}
.track-event:first-child .track-event-dot {
  border-color: var(--bs-primary);
  color: var(--bs-primary);
  background: var(--bs-primary-bg-subtle, var(--bs-body-bg));
}
.track-event-body { line-height: 1.35; }

/* ── Skeleton loaders ────────────────────────────────────────────────
   Shimmer rectangles used while content is loading. Respects reduced-motion
   (the keyframes shimmer becomes a flat gradient when prefers-reduced-motion
   is on — see the consolidated @media at the bottom of this file).
   Color tokens use Bootstrap CSS vars so dark mode just works. */
.skel {
  position: relative;
  overflow: hidden;
  background: var(--bs-secondary-bg);
  border-radius: 0.375rem;
}
.skel::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0.5) 50%,
    rgba(255, 255, 255, 0) 100%
  );
  transform: translateX(-100%);
  animation: skel-shimmer 1.4s ease-in-out infinite;
}
[data-bs-theme="dark"] .skel::after {
  background: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0.12) 50%,
    rgba(255, 255, 255, 0) 100%
  );
}
@keyframes skel-shimmer {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}
.skel-text {
  height: 0.85em;
  width: 100%;
  margin: 0.25em 0;
}
.skel-pill {
  display: inline-block;
  height: 1.1rem;
  width: 4.5rem;
  border-radius: 999px;
}
.skel-row {
  height: 2.5rem;
  width: 100%;
  margin-bottom: 0.4rem;
  border-radius: 0.375rem;
}
.skel-card {
  width: 100%;
  max-width: 320px;
  aspect-ratio: 4 / 6;          /* default portrait — overridable below */
  margin: 0 auto;
  border-radius: var(--bs-border-radius);
  box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.skel-card-portrait  { aspect-ratio: 4 / 6; max-width: 320px; }
.skel-card-landscape { aspect-ratio: 6 / 4; max-width: 480px; }

/* ── Copy-to-clipboard chip ──────────────────────────────────────────
   Small icon button used inline after long codes. Hover/focus reveals it
   without shifting layout; pressed state animates the checkmark flash. */
.copy-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  margin-left: 0.25rem;
  border: 1px solid transparent;
  background: transparent;
  color: var(--bs-secondary-color);
  border-radius: var(--bs-border-radius-sm);
  font-size: 0.78rem;
  line-height: 1;
  vertical-align: middle;
  cursor: pointer;
  transition: background-color var(--pp-t), color var(--pp-t), border-color var(--pp-t), box-shadow var(--pp-t);
}
.copy-chip:hover,
.copy-chip:focus-visible {
  background: var(--bs-tertiary-bg);
  color: var(--bs-body-color);
  border-color: var(--bs-border-color);
  outline: none;
}
.copy-chip-flash {
  background: color-mix(in srgb, var(--bs-success) 14%, transparent);
  border-color: color-mix(in srgb, var(--bs-success) 40%, transparent);
}

/* ── Stagger fade-in ────────────────────────────────────────────────
   Apply .stagger-fade to a list container; children fade up in sequence
   on initial render. The delay caps at the 8th child so a 500-row table
   doesn't stall layout waiting for the last animation to complete. */
@keyframes stagger-fade-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}
.stagger-fade > * {
  animation: stagger-fade-in 0.28s cubic-bezier(.2,.7,.3,1) both;
}
.stagger-fade > *:nth-child(1) { animation-delay: 0ms; }
.stagger-fade > *:nth-child(2) { animation-delay: 50ms; }
.stagger-fade > *:nth-child(3) { animation-delay: 100ms; }
.stagger-fade > *:nth-child(4) { animation-delay: 150ms; }
.stagger-fade > *:nth-child(5) { animation-delay: 200ms; }
.stagger-fade > *:nth-child(6) { animation-delay: 250ms; }
.stagger-fade > *:nth-child(7) { animation-delay: 300ms; }
.stagger-fade > *:nth-child(8) { animation-delay: 350ms; }
.stagger-fade > *:nth-child(n+9) { animation-delay: 400ms; }

/* Status pill pulse — reuses the PDF pulse keyframe with a friendlier
   alias name for any "in-flight" status badge (in_production, etc.). */
.pill-pulse { animation: pdf-pill-pulse 1.6s ease-in-out infinite; }

/* ── Fulfillment status pills ──
   Distinct hue per state, ordered so the colors progress visually as the
   order moves through the pipeline. Light-mode shades chosen for ≥4.5:1
   contrast on the fixed text color; dark-mode shades bumped slightly. */
.fs-pill { color: #fff; }
.fs-pill-ordered   { background: #475569; }  /* slate-600 */
.fs-pill-pdf-ready { background: #0369a1; }  /* sky-700 */
.fs-pill-in-prod   { background: #f59e0b; color: #1f1f1f; }  /* amber-500, dark text */
.fs-pill-label     { background: #4f46e5; }  /* indigo-600 */
.fs-pill-shipped   { background: #0e7490; }  /* cyan-700 */
.fs-pill-delivered { background: #047857; }  /* emerald-700 */
.fs-pill-cancelled { background: #be123c; }  /* rose-700 */

[data-bs-theme="dark"] .fs-pill-ordered   { background: #64748b; }
[data-bs-theme="dark"] .fs-pill-pdf-ready { background: #0284c7; }
[data-bs-theme="dark"] .fs-pill-in-prod   { background: #fbbf24; color: #1f1f1f; }
[data-bs-theme="dark"] .fs-pill-label     { background: #6366f1; }
[data-bs-theme="dark"] .fs-pill-shipped   { background: #0891b2; }
[data-bs-theme="dark"] .fs-pill-delivered { background: #059669; }
[data-bs-theme="dark"] .fs-pill-cancelled { background: #e11d48; }

/* Respect the OS-level reduced-motion preference. Disables animations
   and transitions globally without breaking any layout (durations go to
   nearly zero rather than `none` so onfinish handlers still fire). */
@media (prefers-reduced-motion: reduce) {
  /* ⚠ Pixi is excluded, and the exclusion is load-bearing. This reset does not
     disable animation — it FLATTENS it: 0.01ms and a single iteration, so every
     cue fires and finishes before a frame renders. On her that produced exactly
     "the thinking animation starts, but it stops immediately". Because it is
     !important on a universal selector it also outranks her own stylesheet, so
     nothing written in PixiAvatar could survive it.
     :where() keeps the exclusion at zero specificity so the rest of this rule is
     unchanged for everything else. If a browser is too old to parse it the whole
     rule drops, which merely means animations play normally — a benign failure,
     not a broken layout. */
  *:not(:where(.pixi-orb, .pixi-orb *)),
  *:not(:where(.pixi-orb, .pixi-orb *))::before,
  *:not(:where(.pixi-orb, .pixi-orb *))::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  /* The global rule above runs the shimmer once instantly, freezing it off-screen → skeletons look
     dead. Show a static centered highlight instead so they still read as "loading". */
  .skel::after { animation: none !important; transform: translateX(0) !important; }
}

/* ── Command palette ────────────────────────────────────────────────
   Cmd/Ctrl+K launcher. Lives inside a Bootstrap modal; styling here
   replaces the default modal chrome with a Raycast-ish search box. */
/* Global search pill in the top navbar — the visible entry point that opens the
   ⌘K command palette. Styled like a search input (cursor:text) but it's a button. */
.pp-omnisearch {
  display: inline-flex; align-items: center; gap: 0.55rem;
  flex: 0 1 320px; min-width: 0;
  padding: 0.42rem 0.75rem;
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bs-border-radius);
  background: var(--bs-tertiary-bg);
  color: var(--bs-secondary-color);
  font-size: 0.88rem;
  cursor: text;
  transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
}
.pp-omnisearch:hover,
.pp-omnisearch:focus-visible {
  border-color: var(--bs-primary);
  background: var(--bs-body-bg);
  box-shadow: 0 0 0 3px rgba(var(--bs-primary-rgb), 0.12);
  outline: none;
}
.pp-omnisearch > i { flex: none; font-size: 0.9rem; }
.pp-omnisearch-label { flex: 1 1 auto; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pp-omnisearch-kbd {
  flex: none;
  font-size: 0.68rem; font-weight: 600; letter-spacing: 0.02em;
  padding: 0.08rem 0.36rem; border-radius: 5px;
  background: var(--bs-body-bg); border: 1px solid var(--bs-border-color);
  color: var(--bs-secondary-color);
}
/* Mobile: collapse to an icon-only button — the palette IS the mobile search UI. */
@media (max-width: 575.98px) {
  .pp-omnisearch { flex: none; padding: 0.42rem 0.5rem; }
  /* Icon-only collapse was written for the cramped old header pill — the
     sidebar/drawer variant has full width, so it keeps its label. */
  .pp-omnisearch:not(.pp-omnisearch--side) .pp-omnisearch-label,
  .pp-omnisearch-kbd { display: none; }
}

.cmdk-dialog { max-width: 620px; }
.cmdk-dialog .modal-content {
  border-radius: var(--bs-border-radius-lg);
  box-shadow: 0 24px 60px rgba(0,0,0,.18), 0 4px 12px rgba(0,0,0,.08);
  overflow: hidden;
}
.cmdk-input-wrap {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.85rem 1rem;
  border-bottom: 1px solid var(--bs-border-color);
}
.cmdk-input-icon { color: var(--bs-secondary-color); font-size: 0.95rem; }
#cmdkInput {
  flex: 1;
  border: 0;
  outline: 0;
  background: transparent;
  font-size: 1rem;
  color: var(--bs-body-color);
}
#cmdkInput::placeholder { color: var(--bs-secondary-color); opacity: .8; }
#cmdkResults {
  max-height: 60vh;
  overflow-y: auto;
  padding: 0.35rem;
}
.cmdk-empty {
  padding: 1.5rem 1rem;
  text-align: center;
  color: var(--bs-secondary-color);
  font-size: 0.9rem;
}
/* Section header for the grouped browse view (Recent / Jump to). */
.cmdk-group {
  padding: 0.55rem 0.75rem 0.2rem;
  font-size: 0.68rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--bs-secondary-color);
}
.cmdk-group:first-child { padding-top: 0.25rem; }
/* Matched characters within a result title/subtitle. */
.cmdk-hl {
  background: color-mix(in srgb, var(--bs-primary) 24%, transparent);
  color: inherit; border-radius: 3px; padding: 0 0.05em;
}
.cmdk-row {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  width: 100%;
  padding: 0.55rem 0.75rem;
  border: 0;
  background: transparent;
  border-radius: 8px;
  text-align: left;
  cursor: pointer;
  color: var(--bs-body-color);
}
.cmdk-row > .fa-solid { color: var(--bs-secondary-color); flex-shrink: 0; width: 1rem; text-align: center; }
.cmdk-row.active,
.cmdk-row:focus-visible {
  background: color-mix(in srgb, var(--bs-primary) 12%, transparent);
  outline: none;
}
.cmdk-row.active > .fa-solid { color: var(--bs-primary); }
.cmdk-row-title { flex: 0 1 auto; font-size: 0.92rem; }
.cmdk-row-sub {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 0.78rem;
  color: var(--bs-secondary-color);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cmdk-row-kind {
  flex-shrink: 0;
  font-size: 0.7rem;
  color: var(--bs-secondary-color);
  text-transform: uppercase;
  letter-spacing: .04em;
  padding: 0.1rem 0.45rem;
  border: 1px solid var(--bs-border-color);
  border-radius: 999px;
}
.cmdk-footer {
  display: flex;
  gap: 1rem;
  padding: 0.5rem 1rem;
  border-top: 1px solid var(--bs-border-color);
  background: var(--bs-tertiary-bg);
  font-size: 0.72rem;
  color: var(--bs-secondary-color);
}
.cmdk-footer kbd {
  padding: 0.05rem 0.35rem;
  margin-right: 0.15rem;
  background: var(--bs-body-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: 4px;
  font-family: inherit;
  font-size: 0.7rem;
  color: var(--bs-body-color);
}

/* ── Print stylesheet ────────────────────────────────────────────────
   When printing, the matching JS clones the active panel into a fresh
   body-level <div id="printRoot"> and toggles body.printing-* . The
   stylesheet hides every other body child so the print engine only sees
   the clone — sidestepping the app shell's height:100vh constraint that
   was causing duplicate pages. */
@media print {
  body.printing-order,
  body.printing-stats {
    background: #fff !important;
    color: #000 !important;
  }
  /* Free the document from the app shell's viewport-bound height. */
  body.printing-order, body.printing-stats,
  body.printing-order html, body.printing-stats html {
    height: auto !important;
    overflow: visible !important;
  }
  /* Hide every direct child of <body> except the clone. The original
     app shell, login screen, page loader, scripts, etc all stay out. */
  body.printing-order > :not(#printRoot),
  body.printing-stats > :not(#printRoot) {
    display: none !important;
  }
  /* The clone owns the page. Strip leftover overflow/height constraints
     so its content flows freely onto as many pages as needed. */
  #printRoot {
    display: block !important;
    position: static !important;
    height: auto !important;
    overflow: visible !important;
    width: 100% !important;
    background: #fff !important;
    color: #000 !important;
  }
  #printRoot, #printRoot * {
    overflow: visible !important;
    max-height: none !important;
    box-shadow: none !important;
    text-shadow: none !important;
    animation: none !important;
    transition: none !important;
  }
  /* The cloned manager div was originally style="display:none" or had
     scroll wrappers — neutralize that on the clone. */
  #printRoot > div {
    display: block !important;
    height: auto !important;
    overflow: visible !important;
  }
  #printRoot .d-print-none,
  #printRoot .toast-container,
  #printRoot .modal,
  #printRoot .modal-backdrop,
  #printRoot .offcanvas,
  #printRoot .skel,
  #printRoot .tour-overlay {
    display: none !important;
  }
  #printRoot .card {
    border: 1px solid #ccc !important;
    page-break-inside: avoid;
  }
  @page { margin: 0.5in; }
}

/* ── Mobile layout for manager-panel headers and toolbars ─────────────
   Manager panels (anything matching #*Manager inside #mainPanel) use a
   shared pattern: a header row (title + actions) followed by a toolbar
   row (search + filters + column toggle). Both rows use flex-wrap which
   breaks on narrow widths — title sits on one line, actions drop below;
   search bar sits on one line, column toggle drops below. On mobile we
   force them back onto a single row, let the title truncate, and let
   the search input shrink so the trailing controls stay inline. */
@media (max-width: 575.98px) {
  /* The per-panel toolbar rules that used to live here described the OLD
     two-row layout (header row + separate filter row). Those rows are now
     merged into .pp-panel-head, so the selectors only half-matched — and
     one of them (flex-wrap: nowrap !important on
     .d-flex.justify-content-between.flex-wrap) still hit the merged header
     and silently beat the new layout. The header owns this now; see the
     .pp-panel-head block in the <992px media query. */

  /* Tighter overall padding on phone-narrow viewports. */
  #mainPanel .px-3 { padding-left: .65rem !important; padding-right: .65rem !important; }
}

/* Even narrower screens: shrink the orders select font so longer
   labels still fit comfortably on the second row. */
@media (max-width: 419.98px) {
  #ordersManager .form-select-sm { font-size: .8rem; padding-left: .4rem; padding-right: 1.6rem; }
}

/* ── Pull-to-refresh indicator ──────────────────────────────────────
   Anchored just below the topbar. JS translates only #mainPanel during
   pull — the indicator stays put while content slides past it (iOS-
   native pattern). Lottie animation winds backward while pulling and
   loops forward on release. */
#ptrIndicator {
  position: absolute;
  top: 12px;
  left: 50%;
  width: 56px;
  height: 56px;
  transform: translateX(-50%) scale(.45);
  opacity: 0;
  pointer-events: none;
  z-index: 1000;
  will-change: transform, opacity;
}
/* `.ptr-snap` is added when the gesture releases — both the indicator
   and #mainPanel use this transition so they ease back in sync. */
.ptr-snap { transition: transform .42s cubic-bezier(.16, 1, .3, 1); }
.ptr-fade { transition: transform .42s cubic-bezier(.16, 1, .3, 1), opacity .35s ease-out; }
/* mainPanel's transform originates at the top edge so the pull feels
   anchored to the chrome above it. `will-change` is intentionally NOT
   set here — it would create a stacking context that puts the in-panel
   mobile calendar sidebar (z 1043) under the body-level backdrop. JS
   toggles will-change only while a touch gesture is in progress. The
   `touch-action: pan-y` hint tells the browser vertical scroll is the
   default action so it can scroll natively with momentum until our PTR
   handler explicitly preventDefault()s on an engaged pull. */
#mainPanel,
#orderDetailsManager,
#partyDetailsManager { transform-origin: top center; touch-action: pan-y; }

/* Lottie SVG fills the container. Match the page-loader Lottie's
   dark-mode inversion so the logo reads in either theme. */
#ptrIndicator > div { width: 100%; height: 100%; }
[data-bs-theme="dark"] #ptrIndicator { filter: invert(.92); }

/* One-shot bounce on release. Sits on top of whatever inline transform
   commitRefresh sets — JS adds the class right after pinning the
   indicator at scale(1) and removes it once the keyframe completes. */
@keyframes ptr-bounce {
  0%   { transform: translateX(-50%) scale(1);    }
  35%  { transform: translateX(-50%) scale(1.18); }
  70%  { transform: translateX(-50%) scale(.96);  }
  100% { transform: translateX(-50%) scale(1);    }
}
#ptrIndicator.ptr-bounce { animation: ptr-bounce .42s cubic-bezier(.34, 1.56, .64, 1); }

/* ── Help / Wiki page ────────────────────────────────────────────────
   A self-contained article look. Scoped under #helpManager so the styles
   never leak into the rest of the portal. Uses existing Bootstrap CSS
   variables so it follows the light/dark theme automatically. */
#helpManager .help-page {
  max-width: 880px;
  margin: 0 auto;
  color: var(--bs-body-color);
  line-height: 1.6;
}
#helpManager .help-hero {
  margin-bottom: 1.5rem;
}
#helpManager .help-hero-title {
  font-size: 1.85rem;
  font-weight: 700;
  color: var(--bs-emphasis-color);
  margin: 0 0 .35rem;
}
#helpManager .help-hero-sub {
  color: var(--bs-secondary-color);
  margin: 0 0 1.2rem;
  font-size: .95rem;
}
#helpManager .help-toc {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  padding: .65rem 0;
  /* Aether: this is a scroll-under MASK — it must match the PAGE tone it
     covers (the KB article sits directly on the tinted body), not surface
     white, or it reads as a floating white band behind the chips. */
  background: var(--pp-page-bg);
  border-bottom: 1px solid var(--bs-border-color);
  box-shadow: 0 8px 8px -8px rgba(0, 0, 0, .08);
}
#helpManager .help-toc a,
#helpManager .help-toc button {
  display: inline-block;
  padding: .35rem .8rem;
  background: var(--bs-tertiary-bg);
  color: var(--bs-body-color);
  border: 1px solid var(--bs-border-color);
  border-radius: 999px;
  font-size: .82rem;
  text-decoration: none;
  cursor: pointer;
  transition: background-color .12s, border-color .12s, color .12s;
}
#helpManager .help-toc a:hover,
#helpManager .help-toc a:focus-visible,
#helpManager .help-toc button:hover,
#helpManager .help-toc button:focus-visible {
  background: color-mix(in srgb, var(--bs-primary) 10%, transparent);
  border-color: var(--bs-primary);
  color: var(--bs-primary);
  outline: none;
}
/* ⚠ STACKED ABOVE PIXI, NOT UNDER HER. This sat at right/bottom 1.25rem — the
   exact corner .pixi-launcher occupies — and she is 56px to its 44px and rides
   at z-index 1040, so she covered it completely. Jeff: "pixi is hiding the back
   to top button on the kb."
   Placed unconditionally above where she sits rather than behind a :has() or a
   body class: a scroll-to-top button a little higher up is harmless when she is
   not mounted, and one rule cannot fall out of step with her mounting logic.
   right: 26px centres its 44px over her 56px disc, so they read as one column.
   20 (her bottom) + 56 (her height) + 12 (gap) = 88. */
#helpBackToTop {
  position: fixed;
  right: 26px;
  bottom: calc(88px + env(safe-area-inset-bottom));
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--bs-border-color);
  background: var(--bs-primary);
  color: var(--pp-on-primary, #fff);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1rem;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transform: translateY(8px);
  transition: opacity .2s ease, transform .2s ease;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .2);
  z-index: 1030;
}
#helpBackToTop.show {
  opacity: 1;
  pointer-events: auto;
  transform: translateY(0);
}
#helpBackToTop:hover, #helpBackToTop:focus-visible {
  background: color-mix(in srgb, var(--bs-primary) 85%, black);
  outline: none;
}

#helpManager .help-card {
  /* Sticky TOC is ~60px tall (padding + chips). Anchor scroll lands below it. */
  scroll-margin-top: 70px;
}
#helpManager .help-card .card-header h2 {
  color: var(--bs-emphasis-color);
}
#helpManager .help-h3 {
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--bs-emphasis-color);
  margin: 1.2rem 0 .5rem;
}
#helpManager .help-h3:first-child { margin-top: 0; }
#helpManager .help-h4 {
  font-size: .95rem;
  font-weight: 600;
  color: var(--bs-emphasis-color);
  margin: 1rem 0 .45rem;
}
#helpManager .card-body p { margin: 0 0 .85rem; }
#helpManager .card-body ol li,
#helpManager .card-body ul li { margin-bottom: .35rem; }
#helpManager .card-body > *:last-child { margin-bottom: 0; }
#helpManager .accordion-button { font-weight: 600; }
/* Deep-link anchors — every section header + sub-heading gets a "copy link" icon,
   and sub-headings get the same sticky-TOC scroll offset as the section cards. */
#helpManager .help-h3, #helpManager .help-h4 { scroll-margin-top: 70px; }
#helpManager .help-anchor {
  margin-left: .45rem; font-size: .8em; vertical-align: middle; text-decoration: none;
  color: var(--bs-secondary-color); opacity: .28; transition: opacity .12s, color .12s;
}
#helpManager .card-header:hover .help-anchor,
#helpManager .help-h3:hover .help-anchor,
#helpManager .help-h4:hover .help-anchor { opacity: .7; }
#helpManager .help-anchor:hover,
#helpManager .help-anchor:focus-visible { opacity: 1; color: var(--bs-primary); }

#helpManager code {
  background: var(--bs-tertiary-bg);
  color: var(--bs-emphasis-color);
  padding: .08rem .35rem;
  border-radius: 4px;
  font-size: .9em;
}
#helpManager kbd {
  background: var(--bs-emphasis-color);
  color: var(--bs-body-bg);
  padding: .1rem .4rem;
  border-radius: 4px;
  font-size: .82em;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-weight: 600;
}

/* Mockups + screenshot placeholders. */
#helpManager .help-shot {
  margin: 1rem 0 1.4rem;
  padding: 0;
}
#helpManager .help-shot figcaption {
  margin-top: .55rem;
  text-align: center;
  font-size: .82rem;
  color: var(--bs-secondary-color);
  font-style: italic;
}
#helpManager .help-shot-placeholder {
  border: 2px dashed var(--bs-border-color);
  background: var(--bs-tertiary-bg);
  color: var(--bs-secondary-color);
  padding: 2.2rem 1.2rem;
  text-align: center;
  border-radius: var(--bs-border-radius-lg);
  font-size: .88rem;
  font-style: italic;
}
#helpManager .help-shot-mock {
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bs-border-radius-lg);
  padding: 1rem;
  background: var(--bs-body-bg);
  box-shadow: 0 1px 3px rgba(0,0,0,.05);
}
#helpManager .help-shot-mock-card { max-width: 380px; margin: 0 auto; }
#helpManager .help-shot-mock-hdr {
  font-size: .72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--bs-secondary-color);
  margin-bottom: .55rem;
}
#helpManager .help-shot-mock-link {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .45rem .6rem;
  background: var(--bs-tertiary-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: 8px;
  font-size: .82rem;
}
#helpManager .help-shot-mock-link > span {
  flex: 1;
  color: var(--bs-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#helpManager .help-shot-mock-copy {
  background: transparent;
  border: 0;
  color: var(--bs-secondary-color);
  cursor: default;
  font-size: .85rem;
}
#helpManager .help-toolbar-mock {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
  padding: .55rem .75rem !important;
}
#helpManager .help-mock-label {
  font-size: .8rem;
  color: var(--bs-secondary-color);
}
#helpManager .help-mock-btngroup { display: inline-flex; }
#helpManager .help-mock-btngroup .help-mock-btn:first-child  { border-top-right-radius: 0; border-bottom-right-radius: 0; }
#helpManager .help-mock-btngroup .help-mock-btn:last-child   { border-top-left-radius: 0;  border-bottom-left-radius: 0;  margin-left: -1px; }
#helpManager .help-mock-btn {
  background: var(--bs-body-bg);
  border: 1px solid var(--bs-border-color);
  color: var(--bs-body-color);
  padding: .3rem .65rem;
  border-radius: var(--bs-border-radius);
  font-size: .82rem;
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  cursor: default;
}
#helpManager .help-mock-btn.active {
  background: var(--bs-primary);
  border-color: var(--bs-primary);
  color: var(--pp-on-primary, #fff);
}

@media (max-width: 575.98px) {
  #helpManager .help-page { padding: 1rem !important; }
  #helpManager .help-hero-title { font-size: 1.4rem; }
  #helpManager .help-card .card-header h2 { font-size: 1rem; }
}

/* ── Webinar clickable transcript ────────────────────────────────── */
.webinar-transcript-segments {
  max-height: 280px;
  overflow-y: auto;
  padding: 0.5rem;
  border: 1px solid var(--bs-border-color);
  border-radius: 0.375rem;
  background: var(--bs-tertiary-bg);
}
.ts-seg {
  display: block;
  width: 100%;
  text-align: left;
  background: transparent;
  border: 0;
  padding: 0.35rem 0.5rem;
  border-radius: var(--bs-border-radius-sm);
  color: var(--bs-body-color);
  font-size: 0.85rem;
  line-height: 1.45;
  cursor: pointer;
}
.ts-seg:hover { background: var(--bs-body-bg); }
.ts-seg.active {
  background: rgba(var(--bs-primary-rgb), 0.12);
  outline: 1px solid rgba(var(--bs-primary-rgb), 0.35);
  font-weight: 500;
}
.ts-seg-stamp {
  display: inline-block;
  min-width: 3.25rem;
  margin-right: 0.5rem;
  color: var(--bs-secondary-color);
  font-family: var(--bs-font-monospace);
  font-size: 0.78rem;
  font-weight: 600;
  vertical-align: top;
}
.ts-seg.active .ts-seg-stamp { color: var(--bs-primary); }

/* Stacked layout: video and transcript both span the full container width.
   Overrides the col-md-7 / col-md-5 split that the side-by-side mode uses. */
.webinar-grid.stacked > .col-md-7,
.webinar-grid.stacked > .col-md-5 {
  flex: 0 0 100%;
  max-width: 100%;
}
.webinar-grid.stacked .webinar-transcript-segments {
  max-height: 360px;
}

/* When the share-group has no dropdown caret visible, the single share
   button shouldn't keep the btn-group's flattened right edge. */
.webinar-share-group.no-caret > .btn:first-child {
  border-top-right-radius: var(--bs-btn-border-radius, .375rem);
  border-bottom-right-radius: var(--bs-btn-border-radius, .375rem);
}

/* ── Fulfillment hardware status strip (Orders panel, admin) ──
   Live Pi / card-printer / label-printer / scale dots, painted by
   _pollPiStatus (fulfillment.js) from GET /admin/pi/health every ~20s. */
.pp-hw-strip {
  display: flex; align-items: center; gap: 1.1rem; flex-wrap: wrap;
  padding: .5rem .85rem; border-bottom: 1px solid var(--bs-border-color);
  font-size: .8rem; color: var(--bs-body-color);
  cursor: pointer; user-select: none;
  background: var(--bs-tertiary-bg);
}
.pp-hw-strip:hover { background: var(--bs-secondary-bg); }
.pp-hw-lead {
  display: inline-flex; align-items: center; gap: .4rem;
  font-weight: 700; letter-spacing: .01em; color: var(--bs-body-color);
  padding-right: .9rem; margin-right: .1rem; border-right: 1px solid var(--bs-border-color);
}
.pp-hw-lead i { opacity: .6; }
.pp-hw-item { display: inline-flex; align-items: center; gap: .4rem; white-space: nowrap; font-weight: 500; }
.pp-hw-dot {
  width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0;
  background: var(--bs-secondary-color);
  box-shadow: 0 0 0 0 transparent; transition: background .3s, box-shadow .3s;
}
.pp-hw-dot--ok      { background: var(--bs-success); box-shadow: 0 0 0 3px rgba(var(--bs-success-rgb), .18); }
.pp-hw-dot--warn    { background: var(--bs-warning); box-shadow: 0 0 0 3px rgba(var(--bs-warning-rgb), .18); }
.pp-hw-dot--off     { background: var(--bs-danger);  box-shadow: 0 0 0 3px rgba(var(--bs-danger-rgb), .18); }
.pp-hw-dot--unknown { background: var(--bs-secondary-color); opacity: .5; }
.pp-hw-state { font-weight: 700; }
.pp-hw-state--ok      { color: var(--bs-success); }
.pp-hw-state--warn    { color: var(--bs-warning); }
.pp-hw-state--off     { color: var(--bs-danger); }
.pp-hw-state--unknown { color: var(--bs-secondary-color); }
.pp-hw-meta { font-variant-numeric: tabular-nums; opacity: .7; font-size: .74rem; }
/* Boxed variant — used at the top of the order-detail fulfillment section. */
.pp-hw-strip--card { border: 1px solid var(--bs-border-color); border-radius: var(--bs-border-radius-lg); margin-bottom: 1rem; }

/* ══ Pixi — in-portal support agent (TEST only; see js/pixi.js) ══════════════
   Built on the portal's own tokens rather than the bundled widget's hardcoded
   indigo + `all: initial` reset, so it inherits light/dark, the radius scale
   and --pp-t like every other surface. Appended at the end of the file
   deliberately: it's a self-contained component and nothing should out-cascade
   it by being declared later. */
/* The launcher IS Pixi — same apricot disc, same plum figure, same cyan spark as
   the avatar in the open chat, so closing the panel doesn't turn her into a
   different object. It was a primary-blue button with a flat white silhouette;
   she now simply sits in the world at 56px, matching #pixiSlotHeader exactly. */
.pixi-launcher {
  position: fixed; z-index: 1040;
  right: 20px; bottom: calc(20px + env(safe-area-inset-bottom));
  width: 56px; height: 56px; border: none; border-radius: 999px;
  background: transparent; padding: 0;
  cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  /* Plum-tinted rather than primary-tinted: the shadow of HER, not of a button. */
  box-shadow: 0 6px 20px rgba(36, 20, 54, .30);
  transition: transform var(--pp-t), box-shadow var(--pp-t), opacity var(--pp-t);
}
.pixi-launcher:hover  { transform: translateY(-2px); box-shadow: 0 10px 26px rgba(36, 20, 54, .38); }
.pixi-launcher:active { transform: translateY(0) scale(.96); }
body.pixi-open .pixi-launcher { opacity: 0; pointer-events: none; }

.pixi-panel {
  position: fixed; z-index: 1041;
  /* No pinch-zoom inside the chat, matching the rest of the portal. The page
     meta says user-scalable=no, but iOS has ignored that since iOS 10 for
     accessibility — `touch-action` it does honour, and pan-y still leaves the
     log scrolling normally. */
  touch-action: pan-y;
  right: 20px; bottom: calc(20px + env(safe-area-inset-bottom));
  width: 384px; max-width: calc(100vw - 32px);
  height: min(620px, calc(100dvh - 108px));
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--bs-body-bg);
  border: var(--bs-border-width) solid var(--bs-border-color);
  border-radius: var(--bs-border-radius-lg);
  box-shadow: 0 18px 48px rgba(0, 0, 0, .22), 0 2px 8px rgba(0, 0, 0, .10);
  opacity: 0; transform: translateY(12px) scale(.98);
  transition: opacity .18s ease, transform .18s cubic-bezier(.22,.8,.3,1);
}
.pixi-panel.is-open { opacity: 1; transform: none; }

/* ⚠ THERE IS NO HEADER BAR. The sketch had none in either state — just her over
   the top edge and the conversation underneath — and a tinted strip with a name
   in it is the thing that made her look like a widget's accessory rather than
   the point of the panel.
   What survives is a zero-height positioning layer: it hosts her disc and the
   two icon buttons, and paints nothing. Close and New conversation stay because
   the panel cannot be operated without them; they float over the top-right
   instead of sitting in a band. */
.pixi-head {
  position: relative; flex-shrink: 0;
  display: flex; flex-direction: column; align-items: center;
  height: 0; padding: 0; border: none; background: none;
}
.pixi-id { position: relative; flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; }

/* The name is kept for `aria-labelledby` on the dialog and nothing else — it is
   not drawn. clip-path rather than display:none, or it would leave the dialog
   with no accessible name at all. */
.pixi-name {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
/* "Portal support" was a label pretending to be a status. The only version
   worth any pixels is the one that says she is NOT working, so that is the only
   one shown — a small pill under her, out of the flow so it never shifts the
   conversation down. */
.pixi-status { display: none; }
.pixi-status.is-offline {
  display: block; position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
  margin-top: .3rem; padding: .1rem .5rem; white-space: nowrap;
  font-size: .68rem; border-radius: 999px;
  background: var(--bs-warning-bg-subtle); color: var(--bs-warning-text-emphasis);
}

/* The buttons float over the top-right corner rather than living in a band.
   A translucent chip keeps them legible when a message scrolls under them. */
.pixi-head-actions {
  position: absolute; top: .45rem; right: .45rem; z-index: 2;
  display: flex; align-items: center; gap: .15rem;
  border-radius: 999px; background: color-mix(in srgb, var(--bs-body-bg) 78%, transparent);
  backdrop-filter: blur(6px);
}
.pixi-head-actions .pixi-icon-btn { width: 28px; height: 28px; }

/* ── Chat mode: she straddles the top edge ──────────────────────────────
   Greeting keeps her big and INSIDE the panel; the moment there's a
   conversation she shrinks and climbs onto the rim, so she stays the most
   prominent thing on screen rather than becoming a chip in a toolbar.
   The panel stops clipping so she can hang over the edge; nothing else needs
   corner treatment now, because there is no header band to poke through it. */
.pixi-panel.is-chatting { overflow: visible; }

/* She sits ON TOP of the conversation, and the text fades out as it passes
   behind her.
   The log is a scroller that comes after the head in the DOM, so bubbles were
   painting straight over her face on the way past. Raising the head into its
   own stacking context puts her above everything in the log; the ::after is a
   gradient the height of her overhang, pinned to the panel (not to the
   scrolling content), so lines dissolve into the panel background instead of
   sliding under her edge-on. z-index:-1 keeps it inside the head's layer —
   above every bubble, behind her disc. */
.pixi-panel.is-chatting .pixi-head { z-index: 5; }
.pixi-panel.is-chatting .pixi-head::after {
  content: ''; position: absolute; z-index: -1; pointer-events: none;
  top: 0; left: 1px; right: 1px; height: 3.1rem;
  border-radius: var(--bs-border-radius-lg) var(--bs-border-radius-lg) 0 0;
  background: linear-gradient(to bottom,
    var(--bs-body-bg) 0%,
    var(--bs-body-bg) 52%,
    color-mix(in srgb, var(--bs-body-bg) 70%, transparent) 76%,
    transparent 100%);
}
/* Her lower half hangs into the conversation, so the log is padded to keep the
   first bubble clear of her chin. */
.pixi-panel.is-chatting .pixi-log { padding-top: 3.4rem; }

.pixi-icon-btn {
  flex-shrink: 0; width: 30px; height: 30px; border: none; background: none;
  border-radius: var(--bs-border-radius-sm); color: var(--bs-secondary-color);
  cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
  transition: background-color var(--pp-t), color var(--pp-t);
}
.pixi-icon-btn:hover { background: var(--bs-secondary-bg); color: var(--bs-emphasis-color); }

.pixi-log {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  padding: .9rem; display: flex; flex-direction: column; gap: .7rem;
}

/* ── Greeting: she leads, the questions fill the rest ────────────────────
   This block used to be vertically centred (`margin: auto 0`), which split
   the panel's spare height into two voids — 115px of nothing above her head
   and 75px below the last question. Centring is the wrong instinct here:
   the panel is a fixed 612px tall and the content is a fixed shape, so the
   slack is never going away and has to be SPENT, not shared out.
   It is spent three ways: she gets bigger (128px, the largest she ever is),
   she sits near the top under a soft halo so the space above her reads as
   light around her rather than emptiness, and the questions stretch into
   what's left — the leftover height becomes comfortable, easily-tapped rows
   instead of a gap. */
.pixi-welcome {
  margin: 0; flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column;
  text-align: center; padding: .35rem .25rem 0;
  /* Centred on her head so she has a ground to stand on rather than floating
     in a flat rectangle. Two stops, not one: a single soft stop washed out to
     nothing on the dark panel — at .11 over #1a1d24 it was invisible. */
  background: radial-gradient(150px 130px at 50% 78px,
                              rgba(var(--bs-primary-rgb), .20) 0%,
                              rgba(var(--bs-primary-rgb), .07) 45%,
                              transparent 72%);
}
/* A real step down from a 128px disc. At 1.05rem the eye fell off a cliff
   between her and her name. */
.pixi-welcome h3 { font-size: 1.25rem; font-weight: 700; color: var(--bs-emphasis-color); margin: 0 0 .3rem; }
/* A measure cap, not a width — two tidy lines beat three ragged ones. */
.pixi-welcome p  { font-size: .84rem; color: var(--bs-secondary-color); margin: 0 auto .9rem; max-width: 30ch; }
.pixi-chips { display: flex; flex-direction: column; gap: .5rem; flex: 1 1 auto; min-height: 0; }
.pixi-chip {
  /* Grow into the slack, but stop at 58px — past that a single line of text
     floats in the middle of a slab. min-height keeps them thumb-sized when
     the panel is short. */
  flex: 1 1 auto; min-height: 44px; max-height: 58px;
  display: flex; align-items: center; gap: .6rem;
  border: var(--bs-border-width) solid var(--bs-border-color);
  /* A lifted surface, not the panel's own background. On dark the border alone
     was almost invisible, so the rows read as holes rather than buttons. */
  background: var(--bs-tertiary-bg); color: var(--bs-body-color);
  border-radius: var(--bs-border-radius); padding: .5rem .7rem .5rem .85rem;
  font-size: .88rem; line-height: 1.25; text-align: left; cursor: pointer;
  transition: border-color var(--pp-t), background-color var(--pp-t), transform var(--pp-t);
}
/* A chevron, drawn not fonted — it anchors the right end of a wide row and
   says the row goes somewhere. Without it the text hung in dead space. */
.pixi-chip::after {
  content: ''; flex: 0 0 auto; margin-left: auto;
  width: 7px; height: 7px; border: 2px solid currentColor;
  border-left: 0; border-bottom: 0; transform: rotate(45deg);
  opacity: .35; transition: opacity var(--pp-t);
}
.pixi-chip:hover {
  border-color: var(--bs-primary); background: rgba(var(--bs-primary-rgb), .08);
  transform: translateX(2px);
}
.pixi-chip:hover::after { opacity: .8; }
@media (prefers-reduced-motion: reduce) { .pixi-chip:hover { transform: none; } }

/* Short viewports: she is the biggest single thing in the greeting, so she is
   what gives way. Measured — at 128px the block needs 450px of log and a
   640px-tall window only offers 433, which pushed the last question below the
   fold. At 96px it fits again. Below ~560px tall the log simply scrolls, which
   is what a log is for. The halo has to follow her up: it is a background
   gradient positioned in px, so it does not move on its own. */
@media (max-height: 660px) {
  #pixiSlotGreeting { width: 96px; height: 96px; margin: .1rem auto .5rem; }
  .pixi-welcome {
    background: radial-gradient(112px 98px at 50% 58px,
                                rgba(var(--bs-primary-rgb), .20) 0%,
                                rgba(var(--bs-primary-rgb), .07) 45%,
                                transparent 72%);
  }
  .pixi-welcome h3 { font-size: 1.1rem; }
  .pixi-welcome p  { font-size: .8rem; margin-bottom: .6rem; }
}

/* Messages */
.pixi-msg { display: flex; gap: .5rem; align-items: flex-end; }
.pixi-msg.user { justify-content: flex-end; }
.pixi-msg-avatar {
  width: 24px; height: 24px; border-radius: 999px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(var(--bs-primary-rgb), .12); color: var(--bs-primary); font-size: .62rem;
}
.pixi-bubble {
  max-width: 82%; padding: .55rem .75rem; font-size: .85rem; line-height: 1.45;
  border-radius: var(--bs-border-radius-lg);
  background: var(--bs-tertiary-bg); color: var(--bs-body-color);
  overflow-wrap: anywhere;
}
.pixi-msg.user .pixi-bubble {
  background: var(--bs-primary); color: var(--pp-on-primary, #fff);
  border-bottom-right-radius: var(--bs-border-radius-sm);
}
.pixi-msg.pixi .pixi-bubble { border-bottom-left-radius: var(--bs-border-radius-sm); }
.pixi-bubble > :first-child { margin-top: 0; }
.pixi-bubble > :last-child  { margin-bottom: 0; }
.pixi-bubble p  { margin: 0 0 .45em; }
.pixi-bubble ul, .pixi-bubble ol { margin: 0 0 .45em; padding-left: 1.1em; }
.pixi-bubble code {
  background: var(--bs-secondary-bg); padding: .08em .3em;
  border-radius: var(--bs-border-radius-sm); font-size: .92em;
}
.pixi-bubble pre { background: var(--bs-secondary-bg); padding: .5rem .6rem; border-radius: var(--bs-border-radius-sm); overflow-x: auto; }

/* Typing */
.pixi-typing { display: inline-flex; gap: 4px; padding: .15rem 0; }
.pixi-typing i {
  width: 6px; height: 6px; border-radius: 50%; background: var(--bs-secondary-color);
  animation: pixiBlink 1.2s infinite ease-in-out;
}
.pixi-typing i:nth-child(2) { animation-delay: .18s; }
.pixi-typing i:nth-child(3) { animation-delay: .36s; }
@keyframes pixiBlink { 0%, 60%, 100% { opacity: .28; transform: translateY(0); } 30% { opacity: 1; transform: translateY(-2px); } }

/* ── Tables in her answers ────────────────────────────────────────────
   Her replies render through mdRender (marked, GFM on), so a pipe table from
   the query layer arrives as a real <table>. Jeff, on a wall of comma-
   separated figures: "make the data it spits out more attractive and easier
   to read". Figures are tabular-nums so digits line up in a column you can
   scan down, and the whole table scrolls INSIDE the bubble — a wide
   comparison must never make the chat panel itself scroll sideways. */
/* js/pixi.js wraps each table in this after render. */
.pixi-tablewrap { overflow-x: auto; margin: .5rem 0; -webkit-overflow-scrolling: touch; }
.pixi-bubble table {
  width: 100%;
  border-collapse: collapse;
  font-size: .84em;
  font-variant-numeric: tabular-nums;
}
.pixi-bubble table + p { margin-top: .5rem; }
.pixi-bubble thead th {
  text-align: left;
  font-weight: 600;
  font-size: .92em;
  letter-spacing: .01em;
  color: var(--bs-secondary-color);
  border-bottom: var(--bs-border-width) solid var(--bs-border-color);
  padding: .2rem .5rem .3rem 0;
  white-space: nowrap;
}
.pixi-bubble tbody td {
  padding: .28rem .5rem .28rem 0;
  border-bottom: var(--bs-border-width) solid color-mix(in srgb, var(--bs-border-color) 45%, transparent);
  white-space: nowrap;
}
.pixi-bubble tbody tr:last-child td { border-bottom: 0; }
/* ⚠ EVERY cell stays on one line, including the label. The first version gave
   the label column `white-space: normal; width: 99%` so it would take the
   slack — but inside an overflow container, with the numeric columns set to
   nowrap, that collapses the label to its minimum width and "Non-Profit"
   renders one character per line. Let the table be as wide as it needs and
   let .pixi-tablewrap scroll it; that is what the wrapper is for. */
.pixi-bubble thead th:not(:first-child),
.pixi-bubble tbody td:not(:first-child) { text-align: right; padding-left: .6rem; }
/* An empty header cell (the two-column stat table) shouldn't draw a stub. */
.pixi-bubble thead th:empty { border-bottom-color: transparent; }

/* Copy — one per answer, quiet until the bubble is hovered so it doesn't
   compete with the text, but always present for touch (no hover there). */
.pixi-msg.pixi .pixi-bubble { position: relative; }
.pixi-copy {
  position: absolute; top: .3rem; right: .3rem;
  width: 1.7rem; height: 1.7rem; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: var(--bs-border-radius-sm);
  background: transparent; color: var(--bs-secondary-color);
  font-size: .78rem; cursor: pointer;
  opacity: 0; transition: opacity .15s ease, color .15s ease, background .15s ease;
}
.pixi-msg.pixi .pixi-bubble:hover .pixi-copy,
.pixi-copy:focus-visible { opacity: 1; }
.pixi-copy:hover { background: var(--bs-secondary-bg); color: var(--bs-body-color); }
.pixi-copy.is-done { opacity: 1; color: var(--bs-success); }
.pixi-copy.is-failed { opacity: 1; color: var(--bs-warning); }
@media (hover: none) { .pixi-copy { opacity: .55; } }
@media (prefers-reduced-motion: reduce) { .pixi-copy { transition: none; } }

/* Proposed actions — Pixi suggests, the user taps (see js/pixi.js) */
.pixi-actions { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .55rem; }
.pixi-action {
  display: inline-flex; align-items: center; gap: .35rem;
  border: var(--bs-border-width) solid var(--bs-primary);
  background: transparent; color: var(--bs-primary);
  border-radius: 999px; padding: .28rem .7rem; font-size: .76rem; font-weight: 600;
  cursor: pointer; transition: background-color var(--pp-t), color var(--pp-t);
}
.pixi-action:hover { background: var(--bs-primary); color: var(--pp-on-primary, #fff); }
/* ⚠ TOUCH TARGET. Measured in the portal at iPad width: 28px tall. That was
   defensible when an answer carried one chip; these now carry three or four,
   they are the main way to move through a conversation, and 28px is well under
   the 44px a finger needs. Only coarse pointers grow — a mouse keeps the
   compact row, so nothing changes on desktop.

   ⚠ HEIGHT ONLY. Growing the horizontal padding and the font too was the
   obvious first move and it made things worse: the chips got wider, two no
   longer fitted on a line, and the same four answers went from three rows to
   FOUR — about 190px of a phone screen for a row of buttons. A finger needs
   the target to be 44px TALL; it does not need it to be wider. Measured at
   375px: this keeps two per line and the row at two rows. */
@media (pointer: coarse) {
  .pixi-action { min-height: 44px; }
}

/* Unreachable / error card. The 'away learning new tricks' variant is the
   common one and must not read as a fault — an affiliate seeing warning
   yellow assumes they broke something. Neutral surface, brand accent. */
.pixi-offline-card {
  width: 100%; font-size: .8rem;
  border: var(--bs-border-width) solid var(--bs-border-color);
  background: var(--bs-tertiary-bg);
  border-radius: var(--bs-border-radius); padding: .75rem .8rem;
}
.pixi-offline-card.is-away {
  border-color: rgba(var(--bs-primary-rgb), .35);
  background: rgba(var(--bs-primary-rgb), .07);
}
.pixi-offline-head {
  display: flex; align-items: center; gap: .4rem;
  font-weight: 700; color: var(--bs-emphasis-color); margin-bottom: .35rem;
}
.pixi-offline-card.is-away .pixi-offline-head { color: var(--bs-primary); }
.pixi-offline-card p { margin: 0 0 .6rem; color: var(--bs-secondary-color); line-height: 1.45; }
.pixi-offline-actions { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }

/* Composer */
.pixi-composer {
  flex-shrink: 0; display: flex; align-items: flex-end; gap: .5rem;
  /* The home-indicator inset is zeroed by js/pixi.js while the keyboard is up:
     the keyboard already covers that strip, so keeping it would leave the
     composer floating on a dead band above the keys. */
  padding: .65rem .75rem calc(.65rem + var(--pixi-safe-b, env(safe-area-inset-bottom)));
  border-top: var(--bs-border-width) solid var(--bs-border-color);
  background: var(--bs-body-bg);
}
.pixi-composer textarea {
  flex: 1 1 auto; min-width: 0; resize: none; max-height: 132px;
  border: var(--bs-border-width) solid var(--bs-border-color);
  border-radius: var(--bs-border-radius-lg);
  background: var(--bs-body-bg); color: var(--bs-body-color);
  /* ⚠ 16px is a HARD FLOOR on touch, not a style choice. iOS auto-zooms the
     page into any focused field under 16px, and that zoom is what made the
     chat look scaled and made the sheet lurch when the keyboard opened — the
     viewport was being scaled, not just resized. `maximum-scale=1` in the page
     meta does not stop it; iOS ignores that. See the coarse-pointer rule below. */
  padding: .45rem .7rem; font-size: .85rem; line-height: 1.4; font-family: inherit;
  transition: border-color var(--pp-t), box-shadow var(--pp-t);
}
.pixi-composer textarea:focus {
  outline: none; border-color: var(--bs-primary);
  box-shadow: 0 0 0 .18rem rgba(var(--bs-primary-rgb), .18);
}
/* The floor, applied where it matters. Coarse pointer rather than a width
   query, because the trigger is the touch keyboard, not the screen size. */
@media (pointer: coarse) {
  .pixi-composer textarea { font-size: 16px; }
}
.pixi-send {
  flex-shrink: 0; width: 34px; height: 34px; border: none; border-radius: 999px;
  background: var(--bs-primary); color: var(--pp-on-primary, #fff); cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center; font-size: .8rem;
  transition: filter var(--pp-t), opacity var(--pp-t);
}
.pixi-send:hover:not(:disabled) { filter: brightness(1.08); }
.pixi-send:disabled { opacity: .5; cursor: default; }

/* ── Pixi the mascot ─────────────────────────────────────
   She is rendered IN PLACE inside her container. No floating layer and no
   gliding between slots — the handoff's choreography was rejected on sight
   ("the sliding around is dumb, and feels clunky"), so the vendored
   PixiAssistant.jsx that implemented it is unused and PixiAvatar is mounted
   straight into the host. See creator-app/src/pixi/portal-entry.jsx.

   Each host is a circular avatar disc with the figure at ~93% of it. */
.pixi-orb {
  position: relative;                 /* the aura ring is positioned against it */
  /* Its own stacking context, so the ring/ball/glass z-order below stays local
     and cannot interleave with anything the portal puts around it. */
  isolation: isolate;
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0; border-radius: 999px;
  /* ⚠ NO background here. The stone is painted by .pixi-root instead — see the
     note there. Putting it back would put the ring on top of it again. */
  /* She's decoration inside a button — clicks must reach the button itself. */
  pointer-events: none;
  /* The aura colours are registered custom properties (see @property below), so
     they interpolate: violet melts into gold rather than cutting to it. */
  transition: --pixi-a1 .45s ease, --pixi-a2 .45s ease, --pixi-a3 .45s ease;
}

/* ── Optical centring ───────────────────────────────────────
   Centring her BOX in the disc leaves her low and left, because the silhouette
   is not symmetric: the ponytail sweeps right and the sparkle sits top-right,
   so the box centre lands well right of the face and the eye line well below
   the middle. Aiming the box therefore aims the wrong thing. These numbers aim
   the EYES, solved against the live boxes rather than estimated: 46.0% across,
   58.0% down.

   Both are deliberately below centre now. Dead centre read as sitting too high,
   because the eye line is not the visual centre of a head — the hair above it
   needs more room than the chin below — and sitting low-left is what keeps her
   ponytail, which sweeps up and to the RIGHT, clear of the rim. Her silhouette
   now reaches 50px against a 56px ball, so nothing touches the edge at all.

   ⚠ TWO moving parts have to be neutralised before re-measuring this, and both
   of them silently faked a result first time round:
   - The eyes FOLLOW THE CURSOR, so a naive getBoundingClientRect() reads gaze
     as composition and the answer moves with the mouse. That put the horizontal
     solve at -1.3% when the truth is +9.6%.
   - She BOBS, 3.4% of the disc. Frozen at an arbitrary phase, that alone swings
     the eye line between 50.0% and 53.4% — the whole size of the correction.
     Seek pixiBob to 0 and to 0.5 and average; aiming at either endpoint aims at
     an extreme of her idle, not at her.

   The scale is set by the crown, which has to clear the glass at the TOP of the
   bob rather than at rest. Dropping her the 3% bought that room back, so .92 is
   affordable where .87 was needed with the eyes dead centre — she is bigger AND
   lower. Clearance at the bob's peak is 3.4px; .97 leaves only 0.3px, and these
   are pre-lens numbers with the lens pushing outward. A head that flattens
   against the glass once per bob is worse than one sitting slightly small.

   ⚠ Two elements, one job each — do not merge them. `.pixi-root` is the GLASS:
   it stays concentric with the disc, so the lens and the clip are both centred
   on the ball, and it carries no offset of its own. The <svg> INSIDE it carries
   HER position. When one transform did both, moving her to centre her eyes
   dragged the bulge off-centre along with her. */
/* `overflow: visible` because an <svg> clips to its viewport by default — the
   other half of the square crop. With her box now square, the 1080×960 viewBox
   letterboxes inside it, and anything in those bands was being sliced. */
.pixi-orb[id] .pixi-root > svg {
  overflow: visible;
  /* The lens lives HERE, on her, not on the stone — see the note above. It is
     applied before her transform, so its centre is her box's centre and then
     moves with her: ~5px off the ball's centre, which is imperceptible at the
     bulge's scale and worth it to keep the stone's outline exact. */
  filter: url(#pixiLens);
  /* The rim fade rides HER, not the stone — see the note on .pixi-root. It
     therefore moves with her transform, which is harmless: she clears the rim
     by 6px now, so this is a safety net rather than a shape. */
  /* ⚠ These numbers are DERIVED FROM THE TRANSFORM BELOW — change one, redo the
     other. The mask is applied before the transform, so a mask centred at 50%
     rides up-and-right with her and stops being concentric with the ball; that
     is how her hair got out past the rim at the top right. Pre-compensating:
        centre = 50% − t/s,  radius = 50% / s     (s = .94, t = ±4%)
     which is 45.74% / 54.26% at 53.19%. The overflow clip on .pixi-root is the
     hard backstop if this ever drifts again. */
  -webkit-mask-image: radial-gradient(ellipse 53.19% 53.19% at 45.74% 54.26%, #000 86%, rgba(0,0,0,0) 100%);
          mask-image: radial-gradient(ellipse 53.19% 53.19% at 45.74% 54.26%, #000 86%, rgba(0,0,0,0) 100%);
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  /* ⚠ NOT a geometric solve, and re-deriving it from getBoundingClientRect
     will put her back where she was wrong: layout geometry cannot see the
     lens, so the solver happily reported 46%/58% while she visibly rendered
     high and right. Tuned against the RENDERED result.
     These are near-neutral now only because the lens amplitude came down (see
     portal-entry.jsx). A stronger lens shoves her off centre, and the fix for
     that is a gentler lens, not a bigger counter-offset here — chasing it with
     translate/scale is what produced the tilted, looking-down-left version. */
  transform: translate(4%, -4%) scale(.94);
  transition: transform .5s cubic-bezier(.34, 1.3, .64, 1);
}

/* ── Inside the crystal ball ────────────────────────────────────
   A real barrel distortion, not a scale-up: #pixiLens is a displacement-map
   filter defined in portal-entry.jsx (in the document, not a data: URI — see
   the note there on why that matters). It bulges the figure the way glass
   would, strongest around mid-radius and settling to nothing at the rim.

   Only the FIGURE is filtered. Her face is the disc's background showing
   through the even-odd hair path — a flat colour, so distorting it would look
   identical. That also keeps the aura ring, which is a sibling pseudo-element,
   out of the lens where it belongs: the ring is the glass, not what's behind it.

   The lens is centred on the figure box, which the nudge above has already
   optically centred on her FACE — so the magnified spot lands on her, which is
   where you would hold a magnifier anyway. */
.pixi-orb[id] .pixi-root {
  /* ── This element IS the stone ──────────────────────────────
     The opal fill lives here, not on .pixi-orb, and that is the whole reason
     the ring stops showing through her. A pseudo-element ALWAYS paints above
     its own element's background — that is fixed painting order, and no
     z-index can undo it, because negative-z children still paint after the
     parent's background. So while the stone was .pixi-orb's background, the
     ring's tucked-under band was unavoidably on top of it, reading exactly as
     Jeff described: a transparent bubble with the border showing through.
     Moving the fill one level down puts a real layer above the ring.
     ⚠ Do not move the fill back up to .pixi-orb. */
  position: relative; z-index: 1;      /* ring 0 · stone 1 · glass 2 */
  border-radius: inherit;              /* follows the blob */
  background-color: var(--pixi-orb-bg, transparent);
  background-image:
    /* key light, top-left */
    radial-gradient(circle at 30% 22%, rgba(255,255,255,.60), rgba(255,255,255,0) 58%),
    /* Opal fire: two broad, low-opacity washes in opposed corners and opposed
       hues. Kept wide and faint on purpose — an opal shifts, it does not stripe,
       and anything stronger would start colouring her face instead of the stone. */
    radial-gradient(ellipse 72% 58% at 16% 80%, rgba(255,130,205,.30), transparent 62%),
    radial-gradient(ellipse 66% 52% at 84% 26%, rgba(110,250,230,.26), transparent 60%),
    /* terminator — violet, so the shadow stays inside the stone's own colour */
    radial-gradient(circle at 86% 90%, rgba(88, 60, 150, .30), rgba(88, 60, 150, 0) 64%);
  box-shadow: var(--pixi-orb-ring, none);
  /* ⚠ NO filter here — it belongs on the <svg>, i.e. on HER. A filter moves
     pixels AFTER the border-radius has clipped the background, so filtering
     this element pushed the stone's own edge outward and it bled past the glass
     as a pale sliver over the ring. The stone is a smooth gradient; lensing it
     changed nothing except break its outline. */
  /* Her edge FADES at the rim rather than being cut. A hard clip-path sliced
     the ponytail — it reaches furthest at ~312°, where it crosses the ball by
     about 2px before the lens even pushes it further out, and no amount of
     scaling bought real clearance without making her small. A fade is also the
     truer answer: nothing inside a glass sphere ends in a straight edge, it
     compresses and disappears into the rim.

     It doubles as what makes the blob safe. The disc's outline breathes, the
     mask does not follow it — but because her edge is transparent out there,
     any mismatch is invisible, so the two never have to stay in sync.

     ⚠ Her box must COVER the disc for any of this to work, hence the 100%/100%
     override of the inline width/height PixiAvatar sets. It used to be 104×92.4
     inside a 112 disc — so the fade, which completes at 56px from the centre,
     ran out of box at 46.2px vertically and the box edge cut a straight line
     first. That was the "awkward square crop": not the mask at all, but the
     SVG's own viewport, which clips to its box unless told not to. */
  /* !important because PixiAvatar sets width/height as INLINE styles, and an
     author rule cannot outrank those any other way. */
  width: 100% !important; height: 100% !important;
  /* THE guarantee that her hair cannot leave the bubble. It clips DESCENDANTS
     to this element's rounded box — so it follows the blob exactly, and unlike
     the soft mask below it is not transformed, so it cannot drift out of
     alignment however she is nudged. The mask makes the meeting soft; this
     makes it certain. The stone's own background is unaffected: overflow clips
     children, not the element's own paint. */
  overflow: hidden;
  /* ⚠ The fade itself is on the <svg> below, NOT here. Masking this element
     would fade the STONE at its rim too, and the ring behind would show
     through the translucent edge — the very thing this layering fixes. Only
     her figure gets softened. */
}

/* ── Backing away from the glass ────────────────────────────
   Her negative states shrink AND flip the lens concave, so she reads as
   retreating into the ball rather than pressed against it. The state class sits
   on .pixi-root itself, so this needs no `:has()`.
   `no` is included with `error`: a refusal is a recoil too, and the shrink sits
   on the outer element while the shake animates the figure inside it, so the
   two compose instead of fighting. The filter swap snaps — filters do not
   interpolate — which suits a flinch. */
/* ⚠ Both the shrink AND the concave lens go on the SVG, not on .pixi-root —
   .pixi-root is the stone now, so scaling it would shrink the whole ball, and
   filtering it would bleed its edge. .71 is her resting .84 times the .84
   recoil. */
.pixi-orb[id] .pixi-root.st-error > svg,
.pixi-orb[id] .pixi-root.st-no    > svg {
  filter: url(#pixiLensIn);
  transform: translate(4%, -4%) scale(.79);
}

/* PixiAvatar hardcodes its ink (#1B2130) as an SVG presentation attribute and
   exposes no colour prop, so she can't follow the theme on her own — and in
   dark mode a near-black figure on a near-black panel all but vanished. A CSS
   `fill`/`stroke` outranks a presentation attribute, so she's recoloured here
   without touching the vendored file.

   The diamond is listed too, but ONLY for its resting colour: the vendored
   .pixi-root.st-answering rule (three classes) and the thinking hue keyframes
   are both more specific / higher priority than these two-class selectors, so
   the gold flash and the spin colour-cycle still win while they're running. */
/* The hair takes a gradient (defined in portal-entry.jsx, since CSS cannot
   declare a paint server) rather than the flat ink: lighter violet where the
   ball's key light falls, near-black away from it, so the mass has volume
   instead of reading as a silhouette. Eyes and brow keep the flat ink. */
.pixi-orb .pixi-hair          { fill: url(#pixiHairGrad); }
.pixi-orb .pixi-eye path      { fill: var(--pixi-ink); }
.pixi-orb .pixi-happy-eyes,
.pixi-orb .pixi-brow-line     { stroke: var(--pixi-ink); }

/* ── The sparkle ────────────────────────────────────────────
   It straddles two backdrops: its top sits on the dark hair, its lower-left arm
   crosses onto the apricot face. A gold sparkle therefore vanished across the
   half that overlapped the disc — warm on warm.
   Two fixes, and the outline is the one that actually makes it robust: a thin
   ink edge means it reads against ANY backdrop, so the fill is free to be
   whatever colour suits the state. paint-order:stroke puts the edge behind the
   fill so the shape doesn't thin out. */
.pixi-orb .pixi-diamond path {
  fill: var(--pixi-a1);
  stroke: var(--pixi-spark-edge);
  stroke-width: 12;
  paint-order: stroke;
  stroke-linejoin: round;
}

/* ── The sparkle carries the state's meaning ────────────────
   She has no mouth, so the sparkle is the only thing that can colour-code how a
   reply went. Green for yes, red for no — read at a glance, before the words.

   There used to be one fill rule per state here, duplicating the hexes. They
   are gone: the rule above fills from --pixi-a1, which the `:has()` table
   further down re-points per state, so the sparkle and the aura ring cannot
   disagree — the ring literally is the sparkle's colour. The ink outline from
   above still applies underneath, so every state holds against both the hair
   and the face.

   Error is deliberately NOT red: "no" already owns red and means a refusal,
   whereas error means something broke. A desaturated slate reads as powered
   down instead. The vendored rule drops the whole group to opacity .35, which
   on the dark hair left it essentially gone, so that is lifted here. */
.pixi-orb .pixi-root.st-error .pixi-diamond      { opacity: .62; }
.pixi-orb .pixi-root.st-error .pixi-diamond path { stroke: var(--pixi-spark-edge); }

/* ── Palette ────────────────────────────────────────────────
   The thing that made her a grey badge rather than a character: her FACE is
   negative space. The hair is one even-odd path, so whatever is behind it shows
   through as skin — meaning the disc colour IS her complexion. A blue-tinted
   disc gave her a blue face, and inverting the ink per theme left her either
   near-black on dark or washed out on light.

   So the disc is now a warm skin tone that does NOT flip with the theme (a real
   profile photo doesn't invert either), the hair is a deep plum that reads
   against it on any page background, and the sparkle is gold instead of the
   same colour as the hair — one deliberate accent so the eye lands on her.
   Being theme-independent is what fixes the dark-mode disappearing act at the
   root rather than papering over it with a stronger tint. */
:root {
  /* ── Opal ───────────────────────────────────────────────────
     She lives in a crystal ball, so she stopped pretending to have skin. The
     disc was apricot, then warm sand; both read as a flesh tone in a glass
     sphere, which is the wrong idea. It is now moonstone — a pale violet-white
     with a faint pink-and-mint iridescence rolling across it (see the sheen
     layers below), which is what an opal actually does.

     Two constraints this still has to satisfy, and does:
     - The old "it cannot be pale, it dissolves into the white panel" rule no
       longer binds: the aura ring and the glass rim give the disc its own hard
       edge, so the disc does not have to earn contrast by being dark.
     - It has to stay OUT OF THE RING'S WAY. A saturated amethyst disc looked
       lovely alone but sat in the same hue as the ring's violet and the two
       muddled. Near-white keeps the ring the only saturated thing on screen. */
  --pixi-skin:  #E4DCF4;
  --pixi-ink:   #2A1550;        /* eyes + brow: deep violet, the hair's mid-tone */
  --pixi-spark-edge: #1F0F3A;   /* the hair's darkest — an outline, not a glow */

  /* ── The aura triad ──────────────────────────────────────────
     ONE table for the whole character. --pixi-a1 is the state's signature
     colour: it fills the sparkle AND leads the ring AND tints the bloom, so
     they can never drift apart. a2/a3 are the sweep it turns through.

     Resting state is a cool cyan→indigo→violet sweep. Cool on purpose: her face
     is warm apricot, so a cool ring is the complement and she separates from it
     instead of blending. Indigo is the portal's own primary, which keeps her
     inside the product's palette rather than beside it. */
  --pixi-a1: #2ED8F0;      /* cyan   — the signature */
  --pixi-a2: #5B5BD6;      /* indigo — brand */
  --pixi-a3: #A855F7;      /* violet */
  --pixi-glint: #EAFBFF;   /* the near-white comet head that travels the ring */
  --pixi-aura-spin: 9s;    /* slow at rest — alive, not busy */
  --pixi-aura-op: .82;

  /* Glass thickness at the rim: a catchlight along the top edge, a shadow along
     the bottom. Fixed px rather than a proportion — a rim highlight is a
     physical thing and stays thin however big the ball is. */
  --pixi-glass-edge: inset 0 1.5px 2px rgba(255,255,255,.55),
                     inset 0 -2px 3px rgba(36,20,54,.22);
}

/* Registered so the aura can TRANSITION between states. Unregistered custom
   properties are un-animatable strings and would snap. Browsers without
   @property just snap, which is the old behaviour — nothing breaks. */
@property --pixi-a1 { syntax: '<color>'; inherits: true; initial-value: #2ED8F0; }
@property --pixi-a2 { syntax: '<color>'; inherits: true; initial-value: #5B5BD6; }
@property --pixi-a3 { syntax: '<color>'; inherits: true; initial-value: #A855F7; }

/* ── State → palette ────────────────────────────────────────
   `:has()` because the state class lives on .pixi-root, which is her CHILD —
   PixiAvatar owns that element and there is no hook on the disc itself.
   Every one of these matches the sparkle colour it drives, so the ring reads as
   that sparkle's light spilling out past her hair. */
.pixi-orb:has(.st-thinking)  { --pixi-a1: #A78BFA; --pixi-a2: #5B5BD6; --pixi-a3: #22D3EE;
                               --pixi-aura-spin: 2.4s; --pixi-aura-op: 1; }
.pixi-orb:has(.st-answering) { --pixi-a1: #A9F2FF; --pixi-a2: #2ED8F0; --pixi-a3: #FFFFFF;
                               --pixi-aura-spin: 1.1s; --pixi-aura-op: 1; }
/* Happy is the one state that has to fight the disc: gold on apricot is warm on
   warm, and a gold-through-orange ring just read as a beige halo. The fuchsia
   tail is what saves it — the ring now has contrast INSIDE itself, so it reads
   as confetti rather than a glow, and it stays clearly distinct from the
   orange-red of "no". */
.pixi-orb:has(.st-happy)     { --pixi-a1: #FFC93C; --pixi-a2: #C026D3; --pixi-a3: #FFE9A3;
                               --pixi-aura-spin: 3.4s; --pixi-aura-op: 1; }
.pixi-orb:has(.st-yes)       { --pixi-a1: #3DDC84; --pixi-a2: #17B26A; --pixi-a3: #A7F3D0;
                               --pixi-aura-spin: 2.6s; --pixi-aura-op: 1; }
.pixi-orb:has(.st-no)        { --pixi-a1: #FF5A52; --pixi-a2: #E0342B; --pixi-a3: #FFC2BE;
                               --pixi-aura-spin: 2.6s; --pixi-aura-op: 1; }
/* Error goes grey AND stops turning. Motion is the strongest signal she is
   working, so taking it away is what "something broke" should look like. */
.pixi-orb:has(.st-error)     { --pixi-a1: #79839B; --pixi-a2: #5A6478; --pixi-a3: #9AA3B6;
                               --pixi-aura-op: .45; }
.pixi-orb:has(.st-error)::before { animation-play-state: paused; }

/* The disc has to hold its own edge against BOTH panels, and that rules out a
   pale skin tone: the light panel is white, so the first (#FBE0C8) attempt
   dissolved into it and only read as a disc on dark. A mid-tone apricot is dark
   enough to sit on white and light enough to glow on near-black, which is the
   only band that works for both without flipping with the theme.
   The soft top-left highlight is what stops it looking like a flat sticker —
   it reads as a lit sphere for one line of CSS, no image.

   ⚠ The disc gets a gradient, but only ever WITHIN the skin band, and the
   shading is placed off her face: measured above, the face sits at ~45% across
   and the eye line at ~63% down, so the highlight goes top-left and the
   terminator bottom-right where there is nothing but cheek. A gradient that
   swept the middle would be a gradient across her complexion. */
/* Only the bloom lives out here. It is an OUTER box-shadow, so it paints past
   the disc and behind everything — the one thing that genuinely belongs on this
   element now that the stone itself has moved inward. */
.pixi-orb[id] {
  box-shadow:
    /* Bloom, tinted by the state. Box-shadow paints strictly OUTSIDE the disc,
       so the glow can be as strong as it likes without touching her face. */
    0 0 14px -2px color-mix(in srgb, var(--pixi-a1) 60%, transparent),
    0 0 34px -8px color-mix(in srgb, var(--pixi-a3) 55%, transparent);
  transition: box-shadow .45s ease;
}

/* ── The aura ring ──────────────────────────────────────────
   The expressive surface. Everything the disc cannot do — saturate, rotate,
   change hue with what she is doing — happens out here, past her hair.

   A conic gradient with a near-white glint at 45% reads as a comet travelling
   the rim rather than a spinning rainbow: one bright head, a coloured tail.
   Masked to an annulus so it never crosses onto her face. Both the inset and
   the mask stops are PERCENTAGES, so the same numbers give a 5px ring on the
   56px launcher and a 10px ring on the 112px greeting — proportion, not size. */
.pixi-orb[id]::before {
  content: '';
  position: absolute; inset: -9%;
  z-index: 0;                          /* ring 0 · stone 1 · glass 2 */
  border-radius: inherit;
  background: conic-gradient(from 0deg,
    var(--pixi-a1) 0%, var(--pixi-a2) 18%, var(--pixi-a3) 41%,
    /* The glint gets a NARROW span on purpose. At 22% of the circle it was a
       broad pale wash that just desaturated the ring; at 8% it reads as one
       bright head with a coloured tail — a comet, not a rainbow. */
    var(--pixi-glint) 45%, var(--pixi-a3) 49%,
    var(--pixi-a2) 74%, var(--pixi-a1) 100%);
  /* The annulus is border-box MINUS content-box, not a circular mask punched
     through the middle. Both boxes follow border-radius, so the ring's inner
     edge tracks the blob as it breathes; a radial mask stayed a circle and cut
     across the drifting outline.

     ⚠ The hole is deliberately SMALLER than the disc, and must stay that way.
     Landing it exactly on the disc's edge opened dark crescents between the two
     — inevitable, because the hole's shape is this element's border-radius
     minus its padding, which cannot equal the disc's own outline no matter how
     it is tuned. So the band tucks 9% of the disc UNDER it and the mismatch has
     nowhere to show. The overlap costs nothing: it lands on the very rim, below
     the figure and below the glass, where the rim shadow already darkens it.

     Element is 118% of the disc; padding percentages resolve against WIDTH on
     all four sides, so p = (0.18 + 2×0.09) / 2 / 1.18 = 15.25%. */
  padding: 15.25%;
  background-origin: border-box;
  -webkit-mask-image: linear-gradient(#000 0 0), linear-gradient(#000 0 0);
          mask-image: linear-gradient(#000 0 0), linear-gradient(#000 0 0);
  -webkit-mask-clip: content-box, border-box;
          mask-clip: content-box, border-box;
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  opacity: var(--pixi-aura-op, .82);
  animation: pixiAuraSpin var(--pixi-aura-spin, 9s) linear infinite;
  transition: opacity .45s ease;
  pointer-events: none;
}
/* The comet surges and drifts rather than turning at a constant rate — it covers
   110° in each of two quarters and only 70° in the other two, so it whips
   through and then coasts. Measured: 4.8× between fastest and slowest, and it
   never stops or reverses.

   The quarters alternate fast/slow, which makes the pattern repeat every HALF
   lap — and that is what keeps the loop seamless. The 100%→0% junction is
   slow→fast, exactly like the one at 50%, so the wrap is structurally
   indistinguishable from an internal junction rather than being a place where
   the easing resets. (A single eased 0→360 turn cannot do this: its ends never
   match, so it visibly hitches once per lap.) */
@keyframes pixiAuraSpin {
  0%   { transform: rotate(0deg);   animation-timing-function: cubic-bezier(.4,.15,.6,.85); }
  25%  { transform: rotate(110deg); animation-timing-function: cubic-bezier(.4,.15,.6,.85); }
  50%  { transform: rotate(180deg); animation-timing-function: cubic-bezier(.4,.15,.6,.85); }
  75%  { transform: rotate(290deg); animation-timing-function: cubic-bezier(.4,.15,.6,.85); }
  100% { transform: rotate(360deg); }
}

/* ── The glass shell ────────────────────────────────────────
   The lens does the bending; this is the surface light. Four layers, top down:
   a small hot specular, the softer bloom around it, a bounce off the far inside
   wall, and rim darkening. The rim is the one doing the most work — a sphere's
   edge compresses and goes dark, and that read is what stops the bulge looking
   like a mistake and starts it looking like glass.

   The specular sits upper-LEFT, on her hair. Two reasons: white on near-black
   is where a highlight reads hardest, and it keeps clear of her eyes (63% down)
   and of the sparkle (upper-right), neither of which should be glazed over.

   This element also carries the response ping — one outward ring when something
   lands. Same element, two jobs, and they belong together: the ping is light
   leaving the ball. Only the one-shot states fire it; `thinking` deliberately
   does not, because a pulse repeating while she works reads as a heartbeat
   rather than an event. */
.pixi-orb[id]::after {
  content: '';
  position: absolute; inset: 0;
  z-index: 2;                          /* glass sits above the stone */
  border-radius: inherit;
  pointer-events: none;
  background:
    /* The specular is an SVG rather than a radial-gradient for two reasons CSS
       cannot give: it is ROTATED −32°, and its edges are a real Gaussian blur.
       On a sphere a highlight stretches tangentially — perpendicular to the
       radius through it — and sitting up-left at 10 o'clock that works out to
       about −32°. An axis-aligned ellipse there reads as a sticker. Two passes:
       a wide soft halo and a tighter hot core, both blurred so nothing has a
       defined edge. */
    url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' width='100' height='100'><defs><filter id='h' x='-60%' y='-60%' width='220%' height='220%'><feGaussianBlur stdDeviation='4.6'/></filter><filter id='c' x='-60%' y='-60%' width='220%' height='220%'><feGaussianBlur stdDeviation='2.2'/></filter></defs><ellipse cx='32' cy='22' rx='21' ry='11' fill='%23fff' opacity='.42' filter='url(%23h)' transform='rotate(-32 32 22)'/><ellipse cx='29.5' cy='18' rx='10.5' ry='4.8' fill='%23fff' opacity='.95' filter='url(%23c)' transform='rotate(-32 29.5 18)'/></svg>"),
    radial-gradient(ellipse 46% 17% at 55% 90%, rgba(255,255,255,.26), rgba(255,255,255,0) 74%),
    radial-gradient(closest-side, rgba(36,20,54,0) 58%, rgba(36,20,54,.20) 88%, rgba(36,20,54,.42) 100%);
  /* Required: the SVG has an intrinsic 100×100, so without this it would paint
     at 100px regardless of the disc's size. */
  background-size: 100% 100%;
  background-repeat: no-repeat;
  box-shadow: var(--pixi-glass-edge);
}

/* ── The blob ───────────────────────────────────────────────
   She is not in a bead of glass, she is in something alive, so the outline
   drifts instead of sitting still. Every deviation is kept within ±7% of a true
   circle: past that it stops reading as a sphere and starts reading as a
   wobbling amoeba, and the lens and specular — which ARE spherical — would stop
   agreeing with it.

   The ring and the glass both take `border-radius: inherit`, so all three
   layers breathe together from this one animation. Nothing else needs to know:
   her own edge is a soft mask (see .pixi-root above), so it does not have to
   track the outline at all. 14s and eased, slow enough that you notice it only
   if you watch for it. */
@keyframes pixiBlob {
  0%, 100% { border-radius: 57% 43% 47% 53% / 53% 46% 54% 47%; }
  25%      { border-radius: 45% 55% 57% 43% / 47% 56% 44% 53%; }
  50%      { border-radius: 53% 47% 43% 57% / 56% 44% 56% 44%; }
  75%      { border-radius: 47% 53% 55% 45% / 44% 55% 45% 56%; }
}
.pixi-orb[id] { animation: pixiBlob 14s ease-in-out infinite; }
/* Restated inside the ping keyframes because an animated box-shadow replaces the
   property outright — the glass edge would blink off for the second otherwise.
   One home for the values, so they cannot drift apart. */
.pixi-orb:has(.st-answering)::after,
.pixi-orb:has(.st-happy)::after,
.pixi-orb:has(.st-yes)::after,
.pixi-orb:has(.st-no)::after {
  animation: pixiAuraPing 1.05s cubic-bezier(.2,.75,.3,1) 1;
}
@keyframes pixiAuraPing {
  0%   { box-shadow: var(--pixi-glass-edge), 0 0 0 0 var(--pixi-a1); }
  100% { box-shadow: var(--pixi-glass-edge), 0 0 0 var(--pixi-ping, 15px) transparent; }
}

/* ⚠ There is deliberately no reduced-motion rule for the disc any more. It used
   to stop the blob and the aura spin, which — combined with the global reset
   above — meant the ring sat dead while the figure inside it moved. She is one
   object: the aura, the blob and the figure either all breathe or she looks
   broken. The global reset now excludes .pixi-orb and everything under it, so
   the whole disc keeps the motion it was authored with. */

/* Launcher, header + greeting: one avatar disc, three sizes. The launcher takes
   the header's exact treatment — it fills its whole button, which is why
   .pixi-launcher is transparent and padding-free.
   ⚠ Disc sizes pair with the figure sizes in portal-entry.jsx's HOSTS map —
   change one, change the other. */
#pixiSlotLauncher {
  width: 56px; height: 56px;
  --pixi-orb-bg: var(--pixi-skin);
  --pixi-orb-ring: inset 0 0 0 1px rgba(36, 20, 54, .28);
}

/* ⚠ SHE SITS ON THE RIM, NOT IN THE BAR. 88px with 40 of it above the panel's
   top edge, so roughly two fifths of her breaks out over the chrome. That
   overhang is the whole point: at 56px inline she read as an icon, and the
   thing people came to talk to was the smallest element on screen.
   The negative margin is measured from the panel edge — .pixi-head therefore
   carries NO top padding in chat mode, or it would push her back down.
   The box-shadow is on the slot, not on .pixi-root, so it never lands inside
   the glass and fights the lens filter. */
#pixiSlotHeader {
  width: 88px; height: 88px;
  /* Measured, not guessed: -40px put only 28px of her above the rim once the
     head's own box was accounted for. -52 lands ~40px — a little under half the
     disc — which is the proportion in the sketch. */
  margin-top: -52px;
  --pixi-orb-bg: var(--pixi-skin);
  --pixi-orb-ring: inset 0 0 0 1px rgba(36, 20, 54, .28);
  --pixi-ping: 22px;
  box-shadow: 0 10px 20px rgba(20, 12, 38, .26), 0 2px 5px rgba(20, 12, 38, .16);
  /* She is re-mounted into this host rather than flown here from the greeting
     (PixiBridge.goTo picks a host; it does not tween between them), so the
     arrival would otherwise be a hard cut. A short settle makes it read as her
     taking her seat. */
  animation: pixiTakeSeat .34s cubic-bezier(.2, .9, .28, 1.06) both;
}
@keyframes pixiTakeSeat {
  from { transform: translateY(-8px) scale(1.14); opacity: .55; }
  to   { transform: none;                         opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  #pixiSlotHeader { animation-duration: .01ms; }
}
#pixiSlotGreeting {
  /* The largest she ever appears. The greeting has the room, and the sketch
     wants her to be the thing you see first. */
  width: 128px; height: 128px; margin: .25rem auto .75rem;
  --pixi-orb-bg: var(--pixi-skin);
  --pixi-orb-ring: inset 0 0 0 1px rgba(36, 20, 54, .24);
  /* Matches the chat-mode disc, a little softer at this size — the two states
     should read as one object that moved, not two different treatments. */
  box-shadow: 0 12px 26px rgba(20, 12, 38, .22), 0 2px 6px rgba(20, 12, 38, .14);
  /* The ping's spread is a length, so unlike the ring it does NOT scale itself.
     She is the hero of the empty chat at this size; the ripple grows to match. */
  --pixi-ping: 30px;
}
/* On dark surfaces a bright disc floats; a hairline lift separates it from the
   panel without dimming her. */
[data-bs-theme="dark"] #pixiSlotLauncher,
[data-bs-theme="dark"] #pixiSlotHeader,
[data-bs-theme="dark"] #pixiSlotGreeting {
  --pixi-orb-ring: inset 0 0 0 1px rgba(255, 255, 255, .22);
}

/* The elapsed counter next to the typing dots. Tabular figures so the number
   doesn't jitter as the digits change — a counter that wobbles reads as noise
   rather than progress. */
.pixi-elapsed {
  margin-left: .5rem; font-size: .72rem; font-variant-numeric: tabular-nums;
  color: var(--bs-secondary-color); opacity: .85; vertical-align: middle;
}
.pixi-elapsed.is-long::after {
  content: ' · still working'; opacity: .8;
}

/* Send becomes Stop mid-flight. Kept the same size and position so it's the
   same target under the thumb, only recoloured. */
.pixi-send.is-stop {
  background: var(--bs-secondary-bg, #6c757d);
  color: var(--bs-body-color);
}
.pixi-send.is-stop:hover { filter: brightness(1.08); }

/* Names in her answers are links to the real record — underlined on hover only,
   so a list of parties doesn't turn into a wall of blue rules. */
/* ⚠ This said "underlined on hover only, so a list of parties doesn't turn
   into a wall of blue rules" — and then underlined them ALWAYS, which is
   exactly the wall it set out to avoid. Six underlines in one bullet was the
   ugliest thing in her replies. Colour and weight carry the link; the
   underline arrives on hover. */
.pixi-bubble a[href^="#/"] {
  color: var(--bs-primary); text-decoration: none;
  text-underline-offset: 2px; font-weight: 600;
}
.pixi-bubble a[href^="#/"]:hover,
.pixi-bubble a[href^="#/"]:focus-visible {
  text-decoration: underline; text-decoration-color: currentColor;
}
/* Inside the quiet meta line the link takes the meta's colour until hovered,
   so the row reads as one thing rather than two competing ones. */
.pixi-item-meta a[href^="#/"] { color: inherit; font-weight: 600; }
.pixi-item-meta a[href^="#/"]:hover { color: var(--bs-primary); }

/* Phone: a full-height sheet, not a floating card */
@media (max-width: 575.98px) {
  .pixi-panel {
    right: 0; left: 0; width: auto; max-width: none;
    /* Pinned to the VISUAL viewport, which js/pixi.js keeps in these two
       properties while the panel is open. `position: fixed` is laid out against
       the LAYOUT viewport, and that does not shrink when the keyboard opens —
       iOS scrolls the page to reveal the focused field instead, which slides
       the header off the top. 100dvh does not help either: dvh accounts for
       browser chrome, not the keyboard.
       The fallbacks are what desktop and no-visualViewport browsers get, and
       they are the previous behaviour exactly. */
    top: var(--pixi-vv-top, 0px);
    bottom: auto;
    height: var(--pixi-vv-h, 100dvh);
    border-radius: 0; border-left: none; border-right: none; border-bottom: none;
    transform: translateY(100%);
    /* ⚠ The sheet must EASE to each reported size, not snap to it. iOS reports
       the shrinking viewport in coarse steps while the keyboard slides, so
       applying each one raw is what still read as clunky after the sheet was
       already tracking the right size — the size was right, the motion was not.
       130ms is deliberately shorter than the keyboard's own ~250ms: long enough
       to turn steps into movement, short enough that the sheet still arrives
       with the keys instead of trailing them. The open/close transform keeps
       its own timing. */
    transition: opacity .18s ease,
                transform .18s cubic-bezier(.22, .8, .3, 1),
                height .13s ease-out,
                top .13s ease-out;
  }
  .pixi-panel.is-open { transform: none; }
  .pixi-head { padding-top: calc(.7rem + env(safe-area-inset-top)); }
  .pixi-launcher { right: 14px; bottom: calc(14px + env(safe-area-inset-bottom)); }
}

@media (prefers-reduced-motion: reduce) {
  .pixi-panel, .pixi-launcher { transition: none; }
  .pixi-typing i { animation: none; opacity: .5; }
  /* Nothing extra for Pixi: PixiAvatar disables its own animations here, and
     there's no longer a glide to suppress. */
}

/* ══ Insights ═══════════════════════════════════════════════════════════
   A page that is scanned, not read: the four headline figures carry the
   glance, everything under them is detail you go looking for. Numbers are
   tabular throughout so columns line up and a changing figure doesn't make
   the row jitter. */
.insights-page { max-width: 62rem; }

.insights-grid {
  display: grid; gap: .75rem; margin-bottom: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.insights-stat {
  padding: .9rem 1rem; border-radius: var(--bs-border-radius-lg);
  border: var(--bs-border-width) solid var(--bs-border-color);
  background: var(--bs-tertiary-bg);
}
.insights-stat-value {
  font-size: clamp(1.75rem, 1.2rem + 2vw, 2.5rem); font-weight: 700; line-height: 1.05;
  color: var(--bs-emphasis-color); font-variant-numeric: tabular-nums;
}
.insights-stat-label {
  font-size: .7rem; font-weight: 600; letter-spacing: .09em;
  text-transform: uppercase; color: var(--bs-secondary-color); margin-top: .25rem;
}
.insights-stat-sub { font-size: .78rem; color: var(--bs-success); margin-top: .3rem; font-variant-numeric: tabular-nums; }

/* A callout only appears when something is actually true, so it is allowed to
   be loud enough to read. */
.insights-flag {
  padding: .85rem 1rem; border-radius: var(--bs-border-radius);
  border-left: 4px solid var(--bs-warning); background: var(--bs-warning-bg-subtle);
  color: var(--bs-warning-text-emphasis); margin-bottom: .75rem;
}
.insights-flag--info {
  border-left-color: var(--bs-info); background: var(--bs-info-bg-subtle);
  color: var(--bs-info-text-emphasis);
}
.insights-flag-title { font-weight: 700; margin-bottom: .2rem; }
.insights-flag-body  { font-size: .88rem; }
.insights-flag-nudge { font-size: .85rem; margin-top: .45rem; opacity: .92; }

.insights-section { margin-top: 1.5rem; }
.insights-h2 {
  font-size: .72rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  color: var(--bs-secondary-color); margin-bottom: .5rem;
}
.insights-rows {
  border: var(--bs-border-width) solid var(--bs-border-color);
  border-radius: var(--bs-border-radius-lg); overflow: hidden;
}
.insights-row {
  display: grid; grid-template-columns: 1fr auto; gap: .15rem 1rem;
  padding: .7rem 1rem; align-items: baseline;
}
.insights-row + .insights-row { border-top: var(--bs-border-width) solid var(--bs-border-color); }
.insights-row-label { font-weight: 600; }
.insights-row-value { font-variant-numeric: tabular-nums; font-weight: 600; text-align: right; }
.insights-row-note  { grid-column: 1 / -1; font-size: .8rem; color: var(--bs-secondary-color); }
.insights-money { font-size: 1.1rem; }

/* Wide table scrolls inside its own box — the page body must never scroll
   sideways on a phone. */
.insights-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.insights-chip {
  font-size: .62rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  padding: .1rem .35rem; border-radius: 999px;
  background: var(--bs-secondary-bg); color: var(--bs-secondary-color);
}
.insights-standouts { font-size: .85rem; color: var(--bs-secondary-color); margin: .6rem 0 0; }
.insights-note { font-size: .8rem; color: var(--bs-secondary-color); margin: .5rem 0 0; }
.insights-foot { font-size: .78rem; color: var(--bs-secondary-color); margin: 1.75rem 0 0; }

/* Lifetime / Outstanding — two different questions, so a real control rather
   than one column of numbers trying to answer both. */
.insights-seg {
  display: inline-flex; gap: .15rem; margin-bottom: 1rem; padding: .2rem;
  border: var(--bs-border-width) solid var(--bs-border-color);
  border-radius: 999px; background: var(--bs-tertiary-bg);
}
.insights-seg-btn {
  border: none; background: none; color: var(--bs-secondary-color);
  font: inherit; font-size: .85rem; font-weight: 600;
  padding: .35rem 1rem; min-height: 38px; border-radius: 999px; cursor: pointer;
  display: inline-flex; align-items: center; gap: .4rem;
  transition: background-color var(--pp-t), color var(--pp-t);
}
.insights-seg-btn.is-on { background: var(--bs-body-bg); color: var(--bs-emphasis-color); box-shadow: var(--shadow, 0 1px 2px rgba(0,0,0,.12)); }
.insights-seg-btn:focus-visible { outline: 3px solid var(--bs-primary); outline-offset: 2px; }
.insights-seg-count {
  font-size: .7rem; font-weight: 700; padding: .05rem .4rem; border-radius: 999px;
  background: var(--bs-warning); color: #000; font-variant-numeric: tabular-nums;
}

.insights-opens { display: flex; flex-direction: column; gap: .5rem; }
/* A row is a button because every one of them leads somewhere. */
.insights-open {
  display: grid; grid-template-columns: auto 1fr auto; gap: 0 .9rem; align-items: center;
  width: 100%; text-align: left; cursor: pointer;
  padding: .8rem 1rem; border-radius: var(--bs-border-radius-lg);
  border: var(--bs-border-width) solid var(--bs-border-color);
  background: var(--bs-tertiary-bg); color: var(--bs-body-color);
  font: inherit;
  transition: border-color var(--pp-t), background-color var(--pp-t), transform var(--pp-t);
}
.insights-open:hover { border-color: var(--bs-primary); background: rgba(var(--bs-primary-rgb), .07); transform: translateX(2px); }
.insights-open:focus-visible { outline: 3px solid var(--bs-primary); outline-offset: 2px; }
/* The reminders that would hand the question to Pixi, while she is still a
   private preview. The count and the label are the useful part and stay; the
   row simply is not a control. ⚠ :hover is scoped with :not(.is-inert) above
   would be neater, but this file already leads with the plain rule — undoing
   the affordances here keeps the change to one place and one direction. */
.insights-open.is-inert { cursor: default; }
.insights-open.is-inert:hover { border-color: var(--bs-border-color); background: var(--bs-tertiary-bg); transform: none; }
.insights-open-n {
  font-size: 1.6rem; font-weight: 700; line-height: 1;
  color: var(--bs-emphasis-color); font-variant-numeric: tabular-nums; min-width: 2ch; text-align: right;
}
.insights-open-main { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.insights-open-label { font-weight: 600; }
.insights-open-note  { font-size: .8rem; color: var(--bs-secondary-color); }
.insights-open-go {
  font-size: .72rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--bs-primary); white-space: nowrap;
}
@media (max-width: 34rem) {
  .insights-open { grid-template-columns: auto 1fr; }
  .insights-open-go { grid-column: 2; justify-self: start; }
}
@media (prefers-reduced-motion: reduce) { .insights-open:hover { transform: none; } }

.insights-clear {
  display: flex; align-items: center; gap: 1rem;
  padding: 1.25rem; border-radius: var(--bs-border-radius-lg);
  border: var(--bs-border-width) solid var(--bs-success-border-subtle);
  background: var(--bs-success-bg-subtle); color: var(--bs-success-text-emphasis);
}
.insights-clear-mark { font-size: 2rem; line-height: 1; font-weight: 700; }
.insights-clear-title { font-weight: 700; margin-bottom: .15rem; }
.insights-clear-body  { font-size: .87rem; }

/* Insights — month block header + the money line */
.insights-head { display: flex; align-items: baseline; gap: .6rem; }
.insights-live {
  font-size: .64rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  padding: .12rem .45rem; border-radius: 999px;
  background: var(--bs-success-bg-subtle); color: var(--bs-success-text-emphasis);
}
.insights-owed {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
  margin-top: .75rem; padding: .9rem 1.1rem; border-radius: var(--bs-border-radius-lg);
  border: var(--bs-border-width) solid var(--bs-success-border-subtle);
  background: var(--bs-success-bg-subtle); color: var(--bs-success-text-emphasis);
}
.insights-owed-label { font-weight: 700; }
.insights-owed-note  { font-size: .8rem; opacity: .85; }
.insights-owed-value { font-size: 1.9rem; font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; }

/* A tile you can open. The count and the rows behind it belong together, so
   the list appears under the tiles rather than on another page. */
.insights-stat.is-clickable {
  position: relative; text-align: left; cursor: pointer; font: inherit; width: 100%;
  transition: border-color var(--pp-t), background-color var(--pp-t), transform var(--pp-t);
}
.insights-stat.is-clickable:hover { border-color: var(--bs-primary); background: rgba(var(--bs-primary-rgb), .07); transform: translateY(-1px); }
.insights-stat.is-clickable:focus-visible { outline: 3px solid var(--bs-primary); outline-offset: 2px; }
.insights-stat-go {
  position: absolute; top: .6rem; right: .75rem;
  font-size: .62rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--bs-primary); opacity: 0; transition: opacity var(--pp-t);
}
.insights-stat.is-clickable:hover .insights-stat-go,
.insights-stat.is-clickable:focus-visible .insights-stat-go { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .insights-stat.is-clickable:hover { transform: none; } }

.insights-drill {
  margin-top: 1.25rem; padding: 1rem 1.1rem; border-radius: var(--bs-border-radius-lg);
  border: var(--bs-border-width) solid var(--bs-primary);
  background: var(--bs-body-bg);
}
.insights-drill-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: .6rem; }
.insights-drill-title { font-size: 1rem; font-weight: 700; margin: 0; }

/* ── Parties per month ────────────────────────────────────────────────
   One series, so one hue and no legend — the caption names it. Thin bars,
   rounded data-end anchored to the baseline, a real gap between them, and
   recessive labels: the values are ink, not the series colour. */
.insights-chart { margin: 1rem 0 0; }
.insights-chart-cap {
  font-size: .78rem; color: var(--bs-secondary-color); margin-bottom: .5rem;
}
.insights-chart-note { opacity: .8; }
.insights-bars {
  display: flex; align-items: flex-end; gap: 4px;
  height: 132px; padding: 0 .2rem;
  border-bottom: var(--bs-border-width) solid var(--bs-border-color);
}
.insights-bar-col {
  flex: 1 1 0; min-width: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: flex-end; height: 100%; cursor: default;
}
.insights-bar-val {
  font-size: .7rem; font-weight: 700; color: var(--bs-emphasis-color);
  font-variant-numeric: tabular-nums; line-height: 1.4; min-height: 1.4em;
}
.insights-bar-track { flex: 1 1 auto; width: 100%; display: flex; align-items: flex-end; }
.insights-bar {
  width: 100%; border-radius: 4px 4px 0 0;
  background: var(--bs-primary);
  transition: filter var(--pp-t);
}
.insights-bar-col:hover .insights-bar { filter: brightness(1.15); }
/* The month in progress is partial by definition — striped so a short final
   bar reads as "not finished", not as a collapse. */
.insights-bar.is-partial {
  background: repeating-linear-gradient(45deg,
    var(--bs-primary) 0 4px, color-mix(in srgb, var(--bs-primary) 45%, transparent) 4px 8px);
}
.insights-bar-lab {
  font-size: .66rem; color: var(--bs-secondary-color);
  margin-top: .25rem; white-space: nowrap;
}
.insights-inline-link {
  border: none; background: none; padding: 0; font: inherit; font-size: .8rem;
  color: var(--bs-primary); text-decoration: underline; cursor: pointer;
}
.insights-inline-link:hover { filter: brightness(1.15); }

/* ── Pixi: a list of things should look like a list of things ──────────
   Her list answers were a paragraph of underlined links wrapping mid-name,
   with quotes AND bold AND underline all marking the same word. Each row is
   now a name line and a quiet meta line, and the underline thicket is gone:
   links carry colour and weight, and underline only on hover.  */
.pixi-bubble ul { list-style: none; padding-left: 0; margin: .1rem 0 .5rem; }
.pixi-bubble li.pixi-item {
  display: flex; flex-direction: column; gap: .05rem;
  padding: .45rem .6rem; margin: 0 0 .3rem;
  border-radius: var(--bs-border-radius);
  background: color-mix(in srgb, var(--bs-body-bg) 62%, transparent);
  border-left: 3px solid color-mix(in srgb, var(--bs-primary) 45%, transparent);
}
.pixi-bubble li.pixi-item:last-child { margin-bottom: 0; }
.pixi-item-name { font-weight: 650; line-height: 1.3; }
.pixi-item-meta {
  font-size: .78rem; color: var(--bs-secondary-color); line-height: 1.35;
}
/* Links stop shouting: colour and weight carry them, the underline arrives on
   hover. Six underlines in one bullet was the single ugliest thing here. */
.pixi-bubble a { color: var(--bs-primary); text-decoration: none; font-weight: 600; }
.pixi-bubble a:hover, .pixi-bubble a:focus-visible { text-decoration: underline; }
.pixi-item-meta a { color: inherit; font-weight: 600; }
.pixi-item-meta a:hover { color: var(--bs-primary); }

/* The tally she appends after a list is three facts in one sentence — a
   footnote, not part of the answer. */
.pixi-bubble p.pixi-tally {
  font-size: .78rem; color: var(--bs-secondary-color);
  border-top: var(--bs-border-width) solid var(--bs-border-color);
  padding-top: .45rem; margin-top: .5rem;
}
.pixi-bubble li.pixi-item-plain { list-style: none; margin: .3rem 0 0; }
/* Names in the drill-down are links to the record. Same treatment as Pixi's
   replies: colour and weight carry them, underline on hover — a table of
   permanently underlined names is the wall of blue rules again. */

/* ── Drill-down table ──────────────────────────────────────────────────
   Rebuilt after Jeff's iPad screenshot: the track chip was wrapping to a
   second line on nearly every row, so row heights were ragged and the whole
   thing looked broken. The name column also ate all the width while "Added
   by" was squeezed into a column too narrow to hold a name and a chip.

   Fixes: the chip never wraps, the name column yields, and every other column
   shrinks to its content. */
.insights-drill {
  margin-top: 1.25rem; border-radius: var(--bs-border-radius-lg);
  border: var(--bs-border-width) solid var(--bs-primary);
  background: var(--bs-body-bg); overflow: hidden;
}
.insights-drill-head {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: .8rem 1rem;
  border-bottom: var(--bs-border-width) solid var(--bs-border-color);
  background: var(--bs-tertiary-bg);
}
.insights-drill-title { font-size: 1rem; font-weight: 700; margin: 0; }
/* The list can be 300 rows. It scrolls inside the card so the title and the
   close button stay put — scrolling a long list should never mean losing the
   way out of it. */
/* Vertical only. Under fixed layout every column either wraps or ellipsizes,
   so a horizontal scrollbar has nothing left to reveal — it was appearing for
   a 3px rounding overflow and looked like broken layout. */
.insights-drill-body { max-height: min(60vh, 560px); overflow-y: auto; overflow-x: hidden; }

.insights-table { width: 100%; font-size: .85rem; border-collapse: collapse; }
.insights-table thead th {
  position: sticky; top: 0; z-index: 1;
  background: var(--bs-body-bg);
  font-size: .68rem; font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--bs-secondary-color);
  text-align: left; white-space: nowrap;
  padding: .5rem .75rem;
  box-shadow: inset 0 -1px 0 var(--bs-border-color);
}
.insights-table td { padding: .55rem .75rem; vertical-align: middle; }
.insights-table tbody tr + tr td { border-top: var(--bs-border-width) solid var(--bs-border-color); }
.insights-table tbody tr:hover td { background: var(--bs-tertiary-bg); }

/* Proportions, not "name takes the rest". With the other three columns set to
   nowrap, `width:100%` on the name left it the SMALLEST column — party titles
   were wrapping to three lines while "Added by" sprawled. Explicit shares fix
   the ratio; the name may still wrap on a genuinely long title, which is
   correct, but at two lines rather than four. */
/* ⚠ FIXED layout. Under `auto`, a nowrap cell's content sets its own width and
   the declared percentages are ignored — "Added by" took 268px of a 690px
   table and the party title wrapped to three lines in what was left. */
.insights-table { table-layout: fixed; }
/* ⚠ These must total 100. They summed to 102 and the table overflowed its own
   card — a horizontal scrollbar appeared and the date column was clipped to
   "Aug 11, 20". Under fixed layout the browser does not rescue you. */
.insights-table th:nth-child(1), .insights-cell-name { width: 32%; }
.insights-cell-who   { width: 19%; }
.insights-cell-track { width: 19%; }   /* fits "Photo Managers" once the scrollbar takes its cut */
.insights-cell-meta  { width: 17%; color: var(--bs-secondary-color); font-size: .8rem; }
.insights-table th.insights-num, .insights-table td.insights-num { width: 13%; white-space: nowrap; }
/* The person and their chip stay on one line; a long client name gets an
   ellipsis rather than forcing the row taller. */
.insights-cell-who, .insights-cell-track, .insights-cell-meta {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* In its own column the chip drops the uppercase + letter-spacing it wore as
   an inline badge: "PHOTO MANAGERS" spaced out is ~30% wider than "Photo
   Managers" and was clipping to "PHOTO MANAG…". Nothing is gained by shouting
   it when it has a column header saying Track. */
.insights-cell-track .insights-chip {
  margin-left: 0; text-transform: none; letter-spacing: 0; font-size: .72rem;
}

.insights-table a { color: var(--bs-primary); text-decoration: none; font-weight: 600; }
.insights-table a:hover, .insights-table a:focus-visible { text-decoration: underline; }
.insights-cell-meta a { color: inherit; font-weight: 600; }
.insights-cell-meta a:hover { color: var(--bs-primary); }

/* ⚠ nowrap. "Photo Managers" broke over two lines on almost every row in the
   screenshot, which is what made the table look ragged. */
.insights-table .insights-chip { margin-left: .4rem; white-space: nowrap; }

@media (max-width: 40rem) {
  .insights-table { font-size: .8rem; }
  .insights-table td, .insights-table thead th { padding: .45rem .5rem; }
}

/* ── Drill-down controls ───────────────────────────────────────────────
   Search, a filter per short fixed set, and sortable headers. All operate on
   rows already in the page — the list arrives in one request, so filtering it
   server-side would be slower and no more correct. */
.insights-tools {
  display: flex; gap: .5rem; align-items: center; flex-wrap: wrap;
  padding: .6rem 1rem;
  border-bottom: var(--bs-border-width) solid var(--bs-border-color);
}
.insights-search, .insights-select {
  font: inherit; font-size: .85rem; min-height: 38px;
  padding: .3rem .6rem; border-radius: var(--bs-border-radius);
  border: var(--bs-border-width) solid var(--bs-border-color);
  background: var(--bs-body-bg); color: var(--bs-body-color);
}
.insights-search { flex: 1 1 12rem; min-width: 0; }
.insights-select { flex: 0 0 auto; max-width: 12rem; }
.insights-search:focus, .insights-select:focus {
  outline: none; border-color: var(--bs-primary);
  box-shadow: 0 0 0 3px rgba(var(--bs-primary-rgb), .2);
}
.insights-clear {
  font: inherit; font-size: .8rem; font-weight: 600; min-height: 38px;
  padding: .3rem .7rem; border-radius: var(--bs-border-radius);
  border: var(--bs-border-width) solid var(--bs-border-color);
  background: none; color: var(--bs-secondary-color); cursor: pointer;
}
.insights-clear:hover { color: var(--bs-emphasis-color); border-color: var(--bs-primary); }

/* The whole header cell is the sort control — a 4px arrow is not a hit target. */
.insights-sort {
  display: inline-flex; align-items: center; gap: .3rem;
  border: none; background: none; padding: 0; cursor: pointer;
  font: inherit; font-size: inherit; letter-spacing: inherit;
  text-transform: inherit; color: inherit; font-weight: 700;
}
.insights-sort:hover { color: var(--bs-emphasis-color); }
.insights-sort.is-on { color: var(--bs-primary); }
.insights-sort-mark { font-size: .8em; }
.insights-sort:focus-visible { outline: 2px solid var(--bs-primary); outline-offset: 2px; }

.insights-empty { padding: 1.25rem 1rem; color: var(--bs-secondary-color); text-align: center; }
