/* ============================================================
   AMAG Portal v2 — Design System
   Fonts: DM Sans (body) + DM Mono (code/meta)
   Themes: light | dark | system (via data-theme on <html>)
   RTL: CSS logical properties throughout
   ============================================================ */

/* DM Sans/Mono are the "Classic" theme's fonts, loaded unconditionally. The
   rest are pulled in for the other Appearance-tab design themes (see
   js/themes.js) — one request, so switching themes never triggers a
   render-blocking font fetch. */
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&family=DM+Mono:wght@400;500&family=Inter:wght@400;500;600;700&family=Nunito:wght@400;500;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&family=Poppins:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&family=Work+Sans:wght@400;500;600;700&family=Atkinson+Hyperlegible:wght@400;700&family=Lora:wght@400;500;600;700&family=Manrope:wght@400;500;600;700&display=swap');

/* ── Reset & base ──────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
[hidden] { display: none !important; }

html { font-size: 16px; -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  font-family: var(--font-body);
  font-size: 0.9375rem; line-height: 1.6; min-height: 100vh;
  background: var(--bg-page); color: var(--text-primary);
  transition: background 0.2s, color 0.2s;
}

/* ── Theme tokens ──────────────────────────────────────────── */
:root {
  /* Typography — theme-selectable (not light/dark-dependent); overridden
     inline by the appearance theme engine (see js/themes.js) for any
     theme other than "classic". */
  --font-body:    'DM Sans', system-ui, sans-serif;
  --font-heading: var(--font-body);
}

:root, [data-theme="light"] {
  /* A green that can be READ, not just seen.
     var(--success-text) was used directly for money saved, line totals and category totals.
     It measures 3.3:1 on white — below the 4.5:1 that WCAG AA asks of body text
     — so the very numbers somebody is checking were the hardest thing on the
     page to read. #15803d is 5.0:1 and looks the same at a glance.
     A token rather than a darker hex, because the dark theme needs the opposite
     correction: there, a LIGHTER green is the readable one. */
  --success-text:   #15803d;
  /* Backgrounds — clean white with warm-neutral page bg */
  --bg-page:        #f4f4f6;
  --bg-surface:     #ffffff;
  --bg-surface-2:   #f8f8fa;
  --bg-surface-3:   #efeff2;
  /* Sidebar — deep slate, not pure black */
  --bg-sidebar:     #1c1f2e;
  --bg-sidebar-2:   #252840;
  --bg-sidebar-item:#ffffff14;
  --border-sidebar: rgba(255,255,255,.08);
  --bg-overlay:     rgba(15,17,30,0.5);
  /* Text — high contrast, accessible */
  --text-primary:   #0d0e14;
  --text-secondary: #4a4b5e;
  /* Darkened from #8888a0 on 2026-09-11. That value gave 3.46:1 on a card and
     3.26:1 on a recessed one, against WCAG AA's 4.5:1 — and it is not decorative
     text: folder names under a document, form hints, placeholders and file
     metadata all use it, several at 12-13px where it is hardest to read. This
     value clears 4.5:1 against every surface of every built-in theme (worst
     case 4.57:1), not just this one's, because the token is fixed while each
     theme brings its own backgrounds. Still clearly lighter than
     --text-secondary (8.5:1), so the hierarchy the token exists for survives.
     ThemeContrastValidator now checks it. */
  --text-tertiary:  #686880;
  --text-sidebar:   #e2e4f0;
  --text-sidebar-2: #9098c0;
  --text-on-accent: #ffffff;
  --text-on-red:    #ffffff;
  /* Borders */
  --border:         #e0e0ea;
  --border-strong:  #c4c4d0;
  --border-focus:   #c41a1a;
  /* Brand accent — AMAG red, slightly richer */
  --accent:         #c41a1a;
  --accent-hover:   #a31515;
  --accent-subtle:  #fef0f0;
  --accent-text:    #c41a1a;
  /* Semantic colors */
  --blue:           #1565c0;
  --blue-subtle:    #e8f0fd;
  --blue-text:      #1565c0;
  --green:          #1b7a40;
  --green-subtle:   #e8f5ee;
  --green-text:     #1b7a40;
  --amber:          #a05000;
  --amber-subtle:   #fef5e8;
  --amber-text:     #a05000;
  --red:            #c41a1a;
  --red-subtle:     #fef0f0;
  --red-text:       #c41a1a;
  /* Shadows — slightly more visible */
  --shadow-sm:   0 1px 4px rgba(0,0,0,0.08), 0 0 1px rgba(0,0,0,0.04);
  --shadow-md:   0 4px 16px rgba(0,0,0,0.10), 0 1px 3px rgba(0,0,0,0.06);
  --shadow-lg:   0 8px 32px rgba(0,0,0,0.12), 0 2px 6px rgba(0,0,0,0.06);
  --shadow-xl:   0 20px 60px rgba(0,0,0,0.15), 0 4px 12px rgba(0,0,0,0.08);
  /* Radii — slightly less rounded for a more professional look */
  --radius-sm:   5px; --radius-md: 8px; --radius-lg: 12px;
  --radius-xl:   18px; --radius-pill: 999px;
  --sidebar-w:   240px; --header-h: 56px; --transition: 0.15s ease;
  /* Density — personal preference (data-density on <html>), not part of the
     admin-controlled brand theme. "comfortable" values match what tables/
     cards/nav items already shipped with, so the default is pixel-identical
     to before this existed. */
  --density-row-py:  .75rem;
  --density-card-p:  1.25rem;
  --density-gap:     .5rem;
}

[data-density="compact"] {
  --density-row-py: .4rem;
  --density-card-p: .875rem;
  --density-gap:    .3rem;
}

[data-density="spacious"] {
  --density-row-py: .95rem;
  --density-card-p: 1.5rem;
  --density-gap:    .65rem;
}

[data-theme="dark"] {
  --success-text:   #22c55e;   /* 8.0:1 on the dark surface */
  --bg-page:        #0c0d14;
  --bg-surface:     #14151f;
  --bg-surface-2:   #1c1d2a;
  --bg-surface-3:   #222332;
  --bg-sidebar:     #0a0b12;
  --bg-sidebar-2:   #111220;
  --bg-sidebar-item:#ffffff10;
  --border-sidebar: rgba(255,255,255,.08);
  --bg-overlay:     rgba(0,0,0,0.65);
  --text-primary:   #eeeef8;
  --text-secondary: #9898b4;
  /* Lightened from #585870, which gave 2.61:1 on a card — the worst of the
     three modes and well under AA. In dark mode the two text tiers end up close
     together (4.55:1 here against 4.78:1 for secondary): a fixed token has to
     clear AA against the lightest recessed background any theme defines, and
     there is not much room left below that. Dark mode therefore leans on size
     and weight for hierarchy rather than colour, which is the right way round —
     a distinction nobody can read is not a distinction. */
  --text-tertiary:  #9494b0;
  --text-sidebar:   #d4d6e8;
  --text-sidebar-2: #7078a8;
  --border:         #28293a;
  --border-strong:  #383850;
  --border-focus:   #e53a3a;
  /* --accent darkened slightly from the original #e53a3a — that value gave
     white button text only 4.2:1 contrast against it, short of WCAG AA's
     4.5:1 minimum. #d53636 clears it (4.76:1) with a barely-perceptible
     shift; --accent-hover darkens further the same way light mode's does. */
  --accent:         #d53636;
  --accent-hover:   #c03131;
  --accent-subtle:  #280808;
  --accent-text:    #ff6060;
  --blue:           #60a5fa;
  --blue-subtle:    #0c1a30;
  --blue-text:      #60a5fa;
  --green:          #4ade80;
  --green-subtle:   #0a2010;
  --green-text:     #4ade80;
  --amber:          #fbbf24;
  --amber-subtle:   #201400;
  --amber-text:     #fbbf24;
  --red:            #e53a3a;
  --red-subtle:     #280808;
  --red-text:       #ff6060;
  --shadow-sm:   0 1px 4px rgba(0,0,0,0.30);
  --shadow-md:   0 4px 16px rgba(0,0,0,0.40);
  --shadow-lg:   0 8px 32px rgba(0,0,0,0.50);
  --shadow-xl:   0 20px 60px rgba(0,0,0,0.60);
  --radius-sm:   5px; --radius-md: 8px; --radius-lg: 12px;
  --radius-xl:   18px; --radius-pill: 999px;
  --sidebar-w:   240px; --header-h: 56px; --transition: 0.15s ease;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    /* Same correction as [data-theme="dark"]: on a dark ground the readable
       green is the lighter one. Repeated here rather than inherited, because
       this block exists precisely for readers who have chosen nothing. */
    --success-text:#22c55e;
    --bg-page:#0d0d12;--bg-surface:#16161e;--bg-surface-2:#1e1e28;--bg-surface-3:#252530;
    --bg-sidebar:#0a0a10;--bg-sidebar-2:#13131a;--bg-sidebar-item:#ffffff0e;--border-sidebar:rgba(255,255,255,.08);--bg-overlay:rgba(0,0,0,0.64);
    /* --text-tertiary lightened from #606070 (2.92:1) for the same reason as the
       explicit dark theme above — this is the palette anyone gets who never
       picks a theme and whose OS is in dark mode, so it matters at least as
       much. */
    --text-primary:#ededf5;--text-secondary:#a0a0b8;--text-tertiary:#9494b0;
    --text-sidebar:#d8d8e8;--text-sidebar-2:#7878a0;
    --border:#2a2a38;--border-strong:#3a3a4a;--border-focus:#ff3333;
    /* Same WCAG AA fix as the explicit dark theme above, applied to this
       prefers-color-scheme fallback's own accent (#ff2222 gave 3.82:1). */
    --accent:#e01e1e;--accent-hover:#ca1b1b;--accent-subtle:#2a0808;--accent-text:#ff5555;
    --blue:#5aabff;--blue-subtle:#0a1a2e;--green:#4eca7e;--green-subtle:#0a1e14;
    --amber:#ffb44a;--amber-subtle:#1e1200;--red:#ff4444;--red-subtle:#2a0808;
    --shadow-sm:0 1px 3px rgba(0,0,0,0.3);--shadow-md:0 4px 12px rgba(0,0,0,0.4);
    --shadow-lg:0 8px 32px rgba(0,0,0,0.5);--shadow-xl:0 16px 48px rgba(0,0,0,0.6);
  }
}

/* ── Accessibility ─────────────────────────────────────────── */
:focus-visible { outline: 2px solid var(--border-focus); outline-offset: 2px; border-radius: var(--radius-sm); }
.sr-only { position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0; }
@media (prefers-reduced-motion: reduce) {
  *,*::before,*::after { animation-duration:.01ms!important;transition-duration:.01ms!important; }
  /* animation/transition-duration above doesn't touch scroll-behavior — that's
     a separate property, and native anchor-link/programmatic scrolling would
     otherwise still animate for someone who asked for reduced motion. */
  html,.ai-messages { scroll-behavior:auto; }
}
.skip-link { position:absolute;top:-100%;inset-inline-start:1rem;background:var(--accent);color:#fff;padding:.5rem 1rem;border-radius:var(--radius-sm);font-weight:500;z-index:10000;text-decoration:none; }
.skip-link:focus { top: 1rem; }

/* ── Typography ────────────────────────────────────────────── */
h1 { font-size:1.75rem;font-weight:600;line-height:1.25;font-family:var(--font-heading); }
h2 { font-size:1.375rem;font-weight:600;line-height:1.3;font-family:var(--font-heading); }
h3 { font-size:1.125rem;font-weight:600;line-height:1.35; }
h4 { font-size:.9375rem;font-weight:600; }
h5 { font-size:.875rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-secondary); }
p { margin-block-end:.75rem; }
p:last-child { margin-block-end:0; }
a { color:var(--accent-text);text-decoration:none; }
a:hover { text-decoration:underline; }
code,.mono { font-family:'DM Mono','Fira Code',monospace;font-size:.875em;background:var(--bg-surface-3);padding:.15em .4em;border-radius:var(--radius-sm); }

/* ── App shell ─────────────────────────────────────────────── */
.app-shell { display:flex;min-height:100vh; }

/* ── Sidebar ───────────────────────────────────────────────── */
.sidebar {
  /* --imp-banner-h is set (in px) by core.js's initImpersonationBanner once the
     impersonation banner exists, and stays 0 the rest of the time. A
     position:fixed element takes no part in document flow, so simply
     prepending the banner to <body> never actually pushes this down — it
     used to just rely on the banner's z-index painting over the sidebar's
     top ~48px, which is fragile (anything on a page with an even higher
     z-index than the banner's 500 would cover it back up) and, more to the
     point, doesn't actually free up any space — it only LOOKS like it does.
     Shifting the fixed element's own offset/height is what actually reserves
     room for the banner instead of merely painting over the top of it. */
  width:var(--sidebar-w);height:calc(100vh - var(--imp-banner-h, 0px));background:var(--bg-sidebar);
  display:flex;flex-direction:column;position:fixed;
  inset-block-start:var(--imp-banner-h, 0px);inset-inline-start:0;z-index:200;
  transition:transform var(--transition);overflow:hidden;
}
/* Logo/user header and footer are pinned (flex-shrink:0) and never scroll;
   only .sidebar-nav scrolls internally once its own content — which grows
   with the number of nav items, independent of anything else in the sidebar
   — exceeds the space left after them. min-height:0 overrides the flex-item
   default of min-height:auto, which would otherwise force the sidebar taller
   than 100vh instead of letting this scroll (the classic "flexbug #1"). This
   way a scrollbar can only ever appear in this one middle region, and only
   once it's actually needed — the logo, user info, and footer (profile/sign
   out) never move or resize because of it. */
/* A link: the logo is the way home from every page (2026-09-24), styled not to look like one. */
.sidebar-logo { padding:1.25rem 1.25rem 1rem;border-block-end:1px solid var(--border-sidebar);display:flex;align-items:center;gap:.75rem;min-height:var(--header-h);flex-shrink:0;text-decoration:none;color:inherit; }
.sidebar-logo img { height:32px;width:auto;flex-shrink:0; }
.sidebar-logo:focus-visible { outline:2px solid var(--accent);outline-offset:-2px; }
.sidebar-logo-text { font-size:.875rem;font-weight:600;color:var(--text-sidebar);line-height:1.2; }
.sidebar-user { padding:1rem 1.25rem;border-block-end:1px solid var(--border-sidebar);display:flex;align-items:center;gap:.75rem;flex-shrink:0; }
.sidebar-avatar { width:36px;height:36px;border-radius:50%;background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:center;font-size:.875rem;font-weight:600;flex-shrink:0;overflow:hidden; }
.sidebar-avatar img { width:100%;height:100%;object-fit:cover; }
.sidebar-user-info { flex:1;min-width:0; }
.sidebar-user-name { font-size:.875rem;font-weight:500;color:var(--text-sidebar);white-space:nowrap;overflow:hidden;text-overflow:ellipsis; }
.sidebar-user-role { font-size:.75rem;color:var(--text-sidebar-2); }
.sidebar-nav { flex:1;min-height:0;overflow-y:auto;padding:.5rem 0; }
.sidebar-section-label { padding:.75rem 1.25rem .25rem;font-size:.6875rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--text-sidebar-2); }
.nav-item { display:flex;align-items:center;padding:var(--density-gap) 1.25rem;gap:.625rem;color:var(--text-sidebar-2);cursor:pointer;border-radius:0;transition:background var(--transition),color var(--transition);text-decoration:none;font-size:.9rem;position:relative;user-select:none; }
.nav-item:hover { background:var(--bg-sidebar-item);color:var(--text-sidebar);text-decoration:none; }
.nav-item.active { background:var(--bg-sidebar-item);color:var(--text-sidebar); }
.nav-item.active::before { content:'';position:absolute;inset-inline-start:0;inset-block:0;width:3px;background:var(--accent);border-radius:0 2px 2px 0; }
.nav-item-icon { width:18px;height:18px;flex-shrink:0;opacity:.75; }
.nav-item.active .nav-item-icon { opacity:1; }
.nav-item-chevron { margin-inline-start:auto;transition:transform var(--transition); }
.nav-item.open .nav-item-chevron { transform:rotate(180deg); }
.nav-item-badge { margin-inline-start:auto;background:var(--accent);color:#fff;font-size:.7rem;font-weight:600;padding:.1rem .45rem;border-radius:var(--radius-pill); }
.nav-sub { max-height:0;overflow:hidden;transition:max-height .25s ease; }
.nav-sub.open { max-height:400px; }
.nav-sub .nav-item { padding-inline-start:3rem;font-size:.875rem; }
.sidebar-footer { padding:.5rem .75rem;border-block-start:1px solid var(--border-sidebar);display:flex;flex-wrap:wrap;gap:.5rem;align-items:center; }
/* The version sits on its own line under the two buttons, not squeezed between them. */
/* The version, which is the first thing support asks for.
   It was dimmed to 75% opacity to keep it quiet, which put it at 3.89:1 against
   the sidebar — under the 4.5:1 body text needs, on the one string somebody
   reads aloud down a phone line. 0.85 measures 4.59:1 and is still visibly
   secondary. Quiet is a design goal; unreadable is not. */
.sidebar-version { flex:0 0 100%;text-align:center;font-size:.6875rem;color:var(--text-sidebar-2);opacity:.85;text-decoration:none;padding-block-start:.15rem; }
.sidebar-version:hover { opacity:1; }
.sidebar-footer-btn { flex:1;display:flex;align-items:center;gap:.5rem;padding:.5rem;border-radius:var(--radius-sm);background:transparent;border:none;color:var(--text-sidebar-2);cursor:pointer;font-size:.8125rem;font-family:inherit;transition:background var(--transition),color var(--transition); }
.sidebar-footer-btn:hover { background:var(--bg-sidebar-item);color:var(--text-sidebar); }

/* ── Main content ──────────────────────────────────────────── */
.main-content { flex:1;min-width:0;margin-inline-start:var(--sidebar-w);display:flex;flex-direction:column;min-height:100vh; }
.page-header { background:var(--bg-surface);border-block-end:1px solid var(--border);padding:0 1.5rem;height:var(--header-h);display:flex;align-items:center;gap:1rem;position:sticky;top:0;z-index:100; }
.page-header-title { font-size:1rem;font-weight:600;flex:1; }
.page-body { flex:1;padding:1.5rem;width:100%; }
/* minmax(0,1fr), not 1fr: a 1fr track's automatic minimum is its MIN-CONTENT,
   so a wide table inside made the track wider than the grid box and the whole
   page scrolled sideways rather than the table scrolling inside its own
   .table-wrap. My Certifications did exactly that between 769px (where the
   sidebar is still 264px) and about 1000px. */
.portal-layout { display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:1.5rem;align-items:start; }
.portal-layout.full-width { grid-template-columns:minmax(0,1fr); }

/* ── Impersonation banner ──────────────────────────────────── */
/* Above the modal layer (1100), deliberately. The way out of impersonation has
   to stay reachable whatever else is on screen: a mandatory announcement
   acknowledgement renders a full-screen overlay at 1100, and with the banner
   below it an administrator impersonating someone who had an unacknowledged
   notice could not leave impersonation at all — the only way out was to
   acknowledge, as that person, a notice addressed to them, which writes a
   false acknowledgement in their name. Found by the end-to-end run, which
   could not click the button. */
.impersonation-banner { background:#b25a00;color:#fff;padding:.5rem 1.5rem;display:flex;align-items:center;gap:1rem;font-size:.875rem;font-weight:500;position:sticky;top:0;z-index:1200; }
.impersonation-banner strong { font-weight:700; }
.impersonation-banner-spacer { flex:1; }
.impersonation-end-btn { background:rgba(255,255,255,.2);border:1px solid rgba(255,255,255,.3);color:#fff;padding:.25rem .75rem;border-radius:var(--radius-sm);cursor:pointer;font-size:.8125rem;font-weight:600;font-family:inherit;transition:background var(--transition); }
.impersonation-end-btn:hover { background:rgba(255,255,255,.3); }

/* ── Cookie notice ─────────────────────────────────────────── */
/* BELOW the modal and drawer layers (500/600), not above them. It is pinned to
   the bottom centre of the viewport, which is exactly where a dialog puts its
   Save button — at z-index 900 it floated over the top and swallowed the click.
   The end-to-end run found it the same way it found the impersonation banner
   above: it could not press Save on the profile dialog, three attempts running,
   on an account that had not yet dismissed the notice.
   450 keeps it above the menus (300) and under anything modal. */
.cookie-notice { position:fixed;inset-block-end:1.5rem;inset-inline-start:50%;transform:translateX(-50%);background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-xl);padding:1rem 1.25rem;max-width:480px;width:calc(100vw - 3rem);z-index:450;display:flex;align-items:flex-start;gap:1rem; }
.cookie-notice-text { flex:1;font-size:.875rem;color:var(--text-secondary); }
/* Underlined, not just coloured. A link inside a sentence that is only a
   different colour is invisible to a reader who cannot distinguish that colour
   (WCAG 1.4.1), and axe rates it SERIOUS. This is the consent banner, so the
   links it hides this way are the privacy and cookie policies. */
.cookie-notice-text a { color:var(--accent-text);text-decoration:underline; }

/* ── Cards ─────────────────────────────────────────────────── */
/* min-width:0 because a card in a grid or a flex row must be allowed to be
   narrower than its contents. A grid item's min-width defaults to auto, which
   means min-content — a card holding a wide table makes its whole column that
   wide and the 1fr beside it gets what is left. On Catalog Trends that put
   "Most temp-listed items" 1,000px off the right edge of a 1440px window. The
   table inside already has .table-wrap to scroll in; this is what lets it. */
.card { background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);transition:box-shadow var(--transition);min-width:0; }
.card-header { padding:1rem 1.25rem;border-block-end:1px solid var(--border);display:flex;align-items:center;gap:.75rem; }
.card-title { font-size:.9375rem;font-weight:600;flex:1; }
.card-body { padding:var(--density-card-p); }
.card-footer { padding:.875rem 1.25rem;border-block-start:1px solid var(--border);background:var(--bg-surface-2);border-radius:0 0 var(--radius-lg) var(--radius-lg); }

/* ── Announcement cards ────────────────────────────────────── */
.announcement { border:1px solid var(--border);border-radius:var(--radius-md);overflow:hidden;margin-block-end:.75rem; }

/* The company's allocated currency, where there is nothing to switch between. */
.currency-fixed { font-size:.75rem;font-weight:700;letter-spacing:.04em;
                  padding:.25rem .55rem;border-radius:4px;
                  background:var(--bg-surface-2);color:var(--text-secondary); }

/* ── Integrations ─────────────────────────────────────────────────────────── */
.int-card { border:1px solid var(--border);border-left-width:4px;border-radius:var(--radius-md);
            padding:.9rem 1rem;margin-block-end:.75rem;background:var(--bg-surface);max-width:72ch; }
.int-head { display:flex;align-items:flex-start;gap:.75rem;margin-block-end:.4rem; }
.int-name { font-weight:700;font-size:.95rem; }
.int-purpose { font-size:.8125rem;color:var(--text-secondary);margin-block-start:.15rem; }
.int-summary { font-size:.875rem;margin-block-end:.6rem; }
.int-facts { display:flex;flex-wrap:wrap;gap:.25rem 1.25rem;font-size:.8125rem;
             color:var(--text-secondary); }
.int-label { color:var(--text-tertiary);margin-inline-end:.25rem; }
/* The far end's own words, and the one thing on the card worth reading closely
   when something has gone wrong — so it gets room rather than an ellipsis. */
.int-result { margin-block-start:.6rem;padding:.5rem .6rem;border-radius:6px;
              background:var(--bg-surface-2);font-size:.8125rem;line-height:1.45;
              color:var(--text-secondary);overflow-wrap:anywhere; }
.int-actions { display:flex;flex-wrap:wrap;gap:.4rem;align-items:center;margin-block-start:.75rem; }
.int-msg { font-size:.8125rem;color:var(--text-tertiary); }
.int-badge { font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;
             padding:.2rem .5rem;border-radius:999px;white-space:nowrap;flex-shrink:0; }

.int-card.int-ok      { border-left-color:#16a34a; }
.int-card.int-running { border-left-color:#2563eb; }
.int-card.int-waiting { border-left-color:#94a3b8; }
.int-card.int-stale   { border-left-color:#d97706; }
.int-card.int-broken  { border-left-color:#dc2626; }
.int-card.int-off     { border-left-color:var(--border);opacity:.72; }

.int-badge.int-ok      { background:#dcfce7;color:#14532d; }
.int-badge.int-running { background:#dbeafe;color:#1e3a8a; }
.int-badge.int-waiting { background:var(--bg-surface-2);color:var(--text-secondary); }
.int-badge.int-stale   { background:#fef3c7;color:#92400e; }
.int-badge.int-broken  { background:#fee2e2;color:#7f1d1d; }
.int-badge.int-off     { background:var(--bg-surface-2);color:var(--text-tertiary); }

@media (max-width:600px) { .int-head { flex-wrap:wrap; } }

/* ── The administrator's message on the sign-in page ──────────────────────── */
.login-notice { margin-block-start:1.25rem;padding:.8rem .9rem;border-radius:var(--radius-md);
                border:1px solid var(--border);font-size:.85rem;line-height:1.5;
                background:var(--bg-surface-2);color:var(--text-secondary); }
.login-notice-title { font-weight:700;margin-block-end:.25rem;color:var(--text-primary); }
.login-notice a { text-decoration:underline; }
/* Long addresses are the normal content here, and a cutover notice that pushes
   the sign-in form sideways is worse than no notice. */
.login-notice a { overflow-wrap:anywhere; }
.login-notice-info    { border-color:#93c5fd;background:#eff6ff;color:#1e3a8a; }
.login-notice-success { border-color:#86efac;background:#f0fdf4;color:#14532d; }
.login-notice-warning { border-color:#fde68a;background:#fffbeb;color:#92400e; }
.login-notice-danger  { border-color:#fca5a5;background:#fef2f2;color:#7f1d1d; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .login-notice-info    { background:#172554;color:#dbeafe;border-color:#1e40af; }
  :root:not([data-theme="light"]) .login-notice-success { background:#052e16;color:#dcfce7;border-color:#166534; }
  :root:not([data-theme="light"]) .login-notice-warning { background:#451a03;color:#fef3c7;border-color:#92400e; }
  :root:not([data-theme="light"]) .login-notice-danger  { background:#450a0a;color:#fee2e2;border-color:#991b1b; }
}

/* ── Passkey list (profile) ───────────────────────────────────────────────── */
.pk-row { display:flex;align-items:center;gap:.5rem;padding:.55rem .7rem;
          border:1px solid var(--border);border-radius:var(--radius-md);
          margin-block-end:.5rem;background:var(--bg-surface); }
@media (max-width:600px) { .pk-row { flex-wrap:wrap; } }

/* ── Upcoming training (home panel and /training-schedule.html) ───────────── */
/* No background, matching .announcement directly above it on the home page.
   These cards were --bg-surface (white) sitting on the --bg-page grey while
   every announcement beside them was transparent, so the same column showed
   two different card colours. */
.ts-card { display:flex;gap:.75rem;align-items:flex-start;border:1px solid var(--border);
           border-radius:var(--radius-md);padding:.7rem .85rem;margin-block-end:.6rem; }
.ts-when { color:var(--accent-text);flex-shrink:0;padding-block-start:.1rem; }
.ts-course { font-weight:600;font-size:.9rem; }
.ts-session { font-size:.8rem;color:var(--text-secondary); }
/* A multi-day course gets a line per day, so this stacks rather than wraps
   into one run of text where the 12th and the 13th read as one sentence. */
.ts-meta { font-size:.78rem;color:var(--text-tertiary);margin-block-start:.2rem;
           display:flex;flex-direction:column;gap:.1rem; }
.ts-day  { display:flex;flex-wrap:wrap;align-items:center;gap:.25rem; }
.ts-meta svg { vertical-align:-2px; }
.ts-right { display:flex;flex-direction:column;align-items:flex-end;gap:.4rem;flex-shrink:0; }
/* --bg-surface, not --bg-surface-2: now the card is transparent the pill sits
   on the page grey, and surface-2 (#f8f8fa) against page (#f4f4f6) is two
   shades apart and effectively invisible. */
.ts-seats { font-size:.72rem;font-weight:600;padding:.15rem .45rem;border-radius:999px;
            background:var(--bg-surface);color:var(--text-secondary);white-space:nowrap; }
/* Nearly full is the one thing that changes what somebody does about it. */
.ts-seats.tight { background:#fef3c7;color:#92400e; }
@media (max-width:600px) {
  .ts-card { flex-wrap:wrap; }
  .ts-right { flex-direction:row;align-items:center;width:100%;justify-content:space-between; }
}
.announcement-header { padding:.875rem 1rem;display:flex;align-items:flex-start;gap:.75rem;cursor:pointer; }
.announcement-icon { width:20px;height:20px;flex-shrink:0;border-radius:50%;display:flex;align-items:center;justify-content:center;margin-block-start:1px; }
.announcement[data-type="info"]     .announcement-icon { background:var(--blue-subtle);color:var(--blue); }
.announcement[data-type="warning"]  .announcement-icon { background:var(--amber-subtle);color:var(--amber); }
.announcement[data-type="critical"] .announcement-icon { background:var(--red-subtle);color:var(--red); }
.announcement[data-type="success"]  .announcement-icon { background:var(--green-subtle);color:var(--green); }
.announcement-title { font-weight:600;font-size:.9375rem;flex:1; }
.announcement-date { font-size:.8125rem;color:var(--text-tertiary);white-space:nowrap; }
.announcement-dismiss { background:none;border:none;cursor:pointer;color:var(--text-tertiary);padding:.125rem;border-radius:var(--radius-sm); }
.announcement-dismiss:hover { color:var(--text-secondary); }
.announcement-body { padding:0 1rem 1rem 3rem;font-size:.9rem;color:var(--text-secondary); }
.announcement-body p { margin-block-end:.5rem; }
/* A Quill-authored announcement can contain h1/h2/h3 (an admin picking a
   heading style for a subtitle within the body, not a page title) — left at
   the site-wide heading scale (h2 alone is 1.375rem, ~2.4x this container's
   own .9rem base) it dominates the compact card it's shown in, especially on
   the Dashboard. Scaled down relative to the announcement's own text size,
   not the page's, so the largest heading an admin can reach here still reads
   as "emphasis within this announcement," never as a page-level heading. */
.announcement-body h1, .announcement-body h2, .announcement-body h3 {
  font-size:1.05em; font-weight:600; margin-block:.5rem .35rem; line-height:1.3;
}
.announcement-body h1 { font-size:1.15em; }
.announcement-body.ann-clamped { display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:var(--clamp-lines,10);overflow:hidden; }

/* The same clamp for a training record on the home page. A five-day course
   lists five dates, so one session could stand taller than the three below it
   and the panel stopped being a summary of what is coming up. */
/* Hidden per line, not line-clamped.
   -webkit-line-clamp counts LINE BOXES, and every .ts-day is display:flex — a
   flex container, not a line. So there was nothing for it to count: the rule
   applied, the element computed to flow-root, and all five dates stayed on
   screen looking exactly as though the setting did nothing.
   An announcement body is prose and clamps correctly. A training record is a
   list of discrete lines, so the honest mechanism is to hide the lines. It is
   also exact — "is anything hidden" is a count, not a scrollHeight comparison
   that has to be right about sub-pixel rounding. */
.ts-line-hidden { display:none; }

/* Two columns on the home page, once each record is only two lines tall.
   Eight sessions at five dates each ran to 1,421px of home page; clamped and
   paired they take about a quarter of that.

   The seats-and-enrol column wraps underneath here rather than sitting beside
   the text. At 385px a card cannot afford 83px down the right: the course names
   measure 315px and the date line 350px, and squeezing them was what made two
   columns look impossible. Wrapped, the text gets the full width back. */
/* align-items:start, so a card keeps its own height instead of stretching to
   match whatever is beside it. Expanding one still makes its ROW taller — that
   is what a grid does — but the neighbour is then a short card next to empty
   space rather than a short card stretched tall and mostly blank, which is what
   made the gap look like a fault. */
.ts-home-grid { display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.6rem;align-items:start; }
.ts-home-grid .ts-card { flex-wrap:wrap; }
.ts-home-grid .ts-right { flex-direction:row;align-items:center;width:100%;justify-content:flex-start;gap:.5rem; }

/* One column when there is not room for two — below this the cards are
   narrower than the text they hold, which is the thing being avoided. */
@media (max-width: 1100px) { .ts-home-grid { grid-template-columns:1fr; } }
.ts-more { background:none;border:none;padding:0;margin-block-start:.15rem;font-size:.75rem;color:var(--accent);font-weight:600;cursor:pointer;display:none;align-items:center;gap:.2rem; }
.ts-more.is-needed { display:inline-flex; }

/* ── Video panel ───────────────────────────────────────────── */
.video-panel-item { display:flex;gap:.75rem;padding:.75rem;border-radius:var(--radius-md);cursor:pointer;transition:background var(--transition);align-items:flex-start; }
.video-panel-item:hover { background:var(--bg-surface-2); }
.video-panel-item.playing { background:var(--accent-subtle); }
.video-thumbnail { width:100px;min-width:100px;height:64px;border-radius:var(--radius-sm);object-fit:cover;background:var(--bg-surface-3);position:relative;overflow:hidden; }
.video-thumbnail-play { position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.35); }
.video-thumbnail-play svg { color:#fff;filter:drop-shadow(0 1px 2px rgba(0,0,0,.5)); }
.video-info { flex:1;min-width:0; }
.video-title { font-size:.875rem;font-weight:500;line-height:1.35;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden; }
.video-channel { font-size:.75rem;color:var(--text-tertiary);margin-block-start:.25rem; }

/* Inline video embed (replaces modal) */
.video-inline-embed { padding:.75rem;background:var(--bg-surface-2);border-radius:var(--radius-md);margin-block-end:.5rem; }
.video-inline-embed-player { position:relative;padding-block-start:56.25%;background:#000;border-radius:var(--radius-sm);overflow:hidden;margin-block-end:.625rem; }
.video-inline-embed-player iframe { position:absolute;inset:0;width:100%;height:100%;border:0; }
.video-inline-embed-footer { display:flex;justify-content:space-between;align-items:center;font-size:.875rem; }

/* ── Slide-over Drawer ─────────────────────────────────────── */
.drawer-overlay {
  display:none;position:fixed;inset:0;
  background:var(--bg-overlay);z-index:500;
}
.drawer-overlay.open { display:block; }

.drawer {
  position:fixed;inset-block:0;
  inset-inline-end:-640px;
  width:min(640px,100vw);
  background:var(--bg-surface);
  box-shadow:var(--shadow-xl);
  z-index:600;
  display:flex;flex-direction:column;
  transition:inset-inline-end .3s cubic-bezier(.4,0,.2,1);
  overflow:hidden;
  border-inline-start:1px solid var(--border);
}
.drawer.open { inset-inline-end:0; }
.drawer-lg { inset-inline-end:-860px;width:min(860px,100vw); }
.drawer-lg.open { inset-inline-end:0; }

.drawer-header { padding:1.25rem 1.5rem;border-block-end:1px solid var(--border);display:flex;align-items:center;gap:1rem;background:var(--bg-surface);flex-shrink:0;position:sticky;top:0;z-index:1; }
.drawer-title { font-size:1.0625rem;font-weight:600;flex:1; }
.drawer-close { background:none;border:none;cursor:pointer;padding:.375rem;color:var(--text-tertiary);border-radius:var(--radius-sm);font-size:1.375rem;line-height:1;font-family:inherit;display:flex;align-items:center;transition:color var(--transition),background var(--transition); }
.drawer-close:hover { color:var(--text-primary);background:var(--bg-surface-2); }
.drawer-body { flex:1;overflow-y:auto;padding:1.5rem; }
.drawer-footer { padding:1rem 1.5rem;border-block-start:1px solid var(--border);display:flex;justify-content:flex-end;gap:.75rem;background:var(--bg-surface-2);flex-shrink:0; }

/* ── Upload drop zone ──────────────────────────────────────── */
.upload-zone { border:2px dashed var(--border-strong);border-radius:var(--radius-lg);padding:2.5rem;text-align:center;cursor:pointer;transition:border-color var(--transition),background var(--transition);background:var(--bg-surface-2); }
.upload-zone:hover,.upload-zone.drag-over { border-color:var(--accent);background:var(--accent-subtle); }
.upload-zone-icon { font-size:2rem;margin-block-end:.75rem;opacity:.5; }
.upload-zone-label { font-size:.9375rem;font-weight:500;margin-block-end:.25rem; }
.upload-zone-hint { font-size:.8125rem;color:var(--text-tertiary); }
.upload-progress-track { height:4px;background:var(--bg-surface-3);border-radius:var(--radius-pill);overflow:hidden; }
.upload-progress-fill { height:100%;background:var(--accent);width:0%;transition:width .2s; }
.upload-progress-status { font-size:.8125rem;color:var(--text-secondary);margin-block-start:.375rem; }

/* ── API key display ───────────────────────────────────────── */
.api-key-display { font-family:'DM Mono',monospace;font-size:.875rem;background:var(--bg-surface-3);border:1px solid var(--border);border-radius:var(--radius-md);padding:.75rem 1rem;display:flex;align-items:center;gap:.75rem;word-break:break-all; }
.api-key-value { flex:1;color:var(--text-secondary);letter-spacing:.02em; }

/* ── Buttons ───────────────────────────────────────────────── */
.btn { display:inline-flex;align-items:center;justify-content:center;gap:.375rem;padding:.5rem 1rem;font-size:.875rem;font-weight:500;font-family:inherit;border-radius:var(--radius-md);border:1px solid transparent;cursor:pointer;transition:background var(--transition),border-color var(--transition),box-shadow var(--transition),transform var(--transition);white-space:nowrap;text-decoration:none;min-height:36px;user-select:none; }
.btn:hover { transform:translateY(-1px); }
.btn:active { transform:translateY(0); }
.btn-primary { background:var(--accent);color:#fff;border-color:var(--accent); }
.btn-primary:hover { background:var(--accent-hover);border-color:var(--accent-hover);box-shadow:0 2px 8px rgba(204,0,0,.3); }
.btn-secondary { background:var(--bg-surface);color:var(--text-primary);border-color:var(--border-strong); }
.btn-secondary:hover { background:var(--bg-surface-2); }
.btn-ghost { background:transparent;color:var(--text-secondary);border-color:transparent; }
.btn-ghost:hover { background:var(--bg-surface-2);color:var(--text-primary); }
.btn-danger { background:var(--red-subtle);color:var(--red);border-color:transparent; }
.btn-danger:hover { background:var(--red);color:#fff; }
.btn-sm { padding:.3125rem .625rem;font-size:.8125rem;min-height:30px; }
.btn-lg { padding:.6875rem 1.5rem;font-size:1rem;min-height:44px; }
.btn:disabled,.btn[aria-disabled="true"] { opacity:.5;cursor:not-allowed;transform:none;box-shadow:none; }
.btn-icon { width:36px;height:36px;padding:0;border-radius:var(--radius-md); }
.btn-icon.btn-sm { width:30px;height:30px; }
/* A button that reads as a link — used for the clickable name in a table row,
   where a full button would fight the row. Pairs with .btn. */
/* justify-content matters as much as text-align here: .btn is an inline-flex
   box that centres its content, so a link-button in a fixed-width table cell
   centred its label — and a long company name that is then ellipsised loses its
   BEGINNING, which is the part you read. Left-aligned, a truncated name still
   starts where you expect. */
.btn-link { background:none;border-color:transparent;color:var(--accent-text);padding:0;min-height:0;
            text-align:start;justify-content:flex-start; }
.btn-link:hover { background:none;text-decoration:underline;transform:none; }
.btn-block { width:100%; }

/* ── Forms ─────────────────────────────────────────────────── */
.form-group { margin-block-end:1rem; }
.form-label { display:block;font-size:.875rem;font-weight:500;margin-block-end:.375rem;color:var(--text-secondary); }
.form-label-required::after { content:' *';color:var(--red); }
.form-control { width:100%;padding:.5625rem .75rem;font-size:.9375rem;font-family:inherit;background:var(--bg-surface);color:var(--text-primary);border:1px solid var(--border-strong);border-radius:var(--radius-md);transition:border-color var(--transition),box-shadow var(--transition);line-height:1.5;min-height:40px;appearance:none; }
.form-control:hover { border-color:var(--border-strong); }
.form-control:focus { outline:none;border-color:var(--border-focus);box-shadow:0 0 0 3px rgba(204,0,0,.12); }
.form-control::placeholder { color:var(--text-tertiary); }
.form-control:disabled { background:var(--bg-surface-3);cursor:not-allowed;opacity:.7; }
textarea.form-control { resize:vertical;min-height:80px; }
.form-control-sm { padding:.3125rem .5rem;font-size:.8125rem;min-height:30px; }
select.form-control { background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%239898a8' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right .75rem center;padding-inline-end:2rem; }
.form-hint { font-size:.8125rem;color:var(--text-tertiary);margin-block-start:.25rem; }
.form-error { font-size:.8125rem;color:var(--red);margin-block-start:.25rem; }
.form-control.is-invalid { border-color:var(--red); }
.form-control.is-invalid:focus { box-shadow:0 0 0 3px rgba(204,0,0,.15); }
.icon-picker { display:flex;flex-wrap:wrap;gap:.375rem; }
.icon-picker-btn { display:flex;align-items:center;justify-content:center;width:36px;height:36px;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-surface);color:var(--text-secondary);cursor:pointer; }
.icon-picker-btn:hover { border-color:var(--border-strong);color:var(--text-primary); }
.icon-picker-btn.selected { border-color:var(--accent);color:var(--accent-text);background:var(--accent-subtle); }
.icon-picker-btn:focus-visible { outline:2px solid var(--border-focus);outline-offset:1px; }
.form-check { display:flex;align-items:flex-start;gap:.5rem;padding-block:.125rem; }
.form-check-input { width:17px;height:17px;margin-block-start:1px;flex-shrink:0;accent-color:var(--accent);cursor:pointer; }
.form-check-label { font-size:.9rem;cursor:pointer; }
.toggle { position:relative;display:inline-block;width:40px;height:22px; }
.toggle input { opacity:0;width:0;height:0; }
.toggle-slider { position:absolute;cursor:pointer;inset:0;background:var(--border-strong);border-radius:var(--radius-pill);transition:background var(--transition); }
.toggle-slider::before { content:'';position:absolute;width:16px;height:16px;inset-block-start:3px;inset-inline-start:3px;background:#fff;border-radius:50%;transition:transform var(--transition);box-shadow:var(--shadow-sm); }
.toggle input:checked + .toggle-slider { background:var(--accent); }
.toggle input:checked + .toggle-slider::before { transform:translateX(18px); }
.toggle input:focus-visible + .toggle-slider { outline:2px solid var(--border-focus);outline-offset:2px; }

/* ── Search box ────────────────────────────────────────────── */
/* Sortable column header. A button rather than a click handler on the th, so
   it is reachable by keyboard and announced as something that does anything. */
/* text-transform is NOT part of the `font` shorthand, and the browser's own
   stylesheet sets `text-transform:none` on every form control — so a sortable
   header sat inside an uppercase `thead th` and rendered mixed-case anyway.
   Tables came out reading "Name  Email  ROLE  CAN DO": the two columns with no
   sort button were the only ones the design's uppercase reached. */
.btn-sort { background:none;border:0;padding:0;font:inherit;text-transform:inherit;color:inherit;cursor:pointer;display:inline-flex;align-items:center;gap:.25rem; }
.btn-sort:hover { color:var(--text-primary); }
.btn-sort:focus-visible { outline:2px solid var(--border-focus);outline-offset:2px;border-radius:var(--radius-sm); }

.search-wrap { position:relative; }
.search-wrap .form-control { padding-inline-start:2.5rem; }
.search-icon { position:absolute;inset-block-start:50%;inset-inline-start:.75rem;transform:translateY(-50%);color:var(--text-tertiary);pointer-events:none; }

/* ── Appearance theme picker ───────────────────────────────── */
.theme-card { transition: box-shadow var(--transition), border-color var(--transition); }
.theme-card:hover { box-shadow: var(--shadow-sm); }
.theme-card:focus-visible { outline: 2px solid var(--border-focus); outline-offset: 2px; }

/* ── Badges ────────────────────────────────────────────────── */
.badge { display:inline-flex;align-items:center;gap:.25rem;padding:.1875rem .5rem;font-size:.75rem;font-weight:500;border-radius:var(--radius-pill);white-space:nowrap; }
.badge-default  { background:var(--bg-surface-3);color:var(--text-secondary); }
.badge-accent   { background:var(--accent-subtle);color:var(--accent-text); }
.badge-blue     { background:var(--blue-subtle);color:var(--blue); }
.badge-green    { background:var(--green-subtle);color:var(--green); }
.badge-amber    { background:var(--amber-subtle);color:var(--amber); }
.badge-red      { background:var(--red-subtle);color:var(--red); }
.status-dot { width:8px;height:8px;border-radius:50%;display:inline-block; }
.status-dot-green { background:var(--green); }
.status-dot-amber { background:var(--amber); }
.status-dot-red   { background:var(--red); }
.status-dot-gray  { background:var(--text-tertiary); }

/* ── Tables ────────────────────────────────────────────────── */
.table-wrap { overflow-x:auto;-webkit-overflow-scrolling:touch; }
table { width:100%;border-collapse:collapse;font-size:.9rem; }
thead th { background:var(--bg-surface-2);padding:.625rem .875rem;text-align:start;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-secondary);border-block-end:1px solid var(--border);white-space:nowrap; }
/* Per-column filter row rendered directly under a sortable header row —
   resets the uppercase/bold header styling so an <input>/<select> inside
   reads as a normal control, not as more header text. */
tr.filter-row th { padding:.35rem .5rem;text-transform:none;letter-spacing:normal;font-weight:400;white-space:normal; }
tr.filter-row .filter-input { min-height:30px;padding:.3rem .5rem;font-size:.8125rem; }
tbody tr { border-block-end:1px solid var(--border);transition:background var(--transition); }
tbody tr:last-child { border-block-end:none; }
tbody tr:hover { background:var(--bg-surface-2); }
/* A number and its label do not need the height of a paragraph. The AI Cost
   page leads with four of these, and at the default height the chart below
   them started off the bottom of the screen. */
/* Four across, which is the point of this row — but two on a phone, where a
   quarter of 380px is 86px and a figure like $1,234.56 simply does not fit in
   it. It overflowed its card and took the page with it. Declared as a class so
   the phone case can override it; as an inline style it could only have been
   beaten with !important. */
.stats-grid-4 { grid-template-columns:repeat(4,minmax(0,1fr)); }
@media (max-width:768px) { .stats-grid-4 { grid-template-columns:repeat(2,minmax(0,1fr)); } }

.stat-card-tight { padding:.6rem .8rem; }
.stat-card-tight .stat-value { font-size:1.35rem;line-height:1.2; }
.stat-card-tight .stat-label { font-size:.7rem; }
.stat-card-tight .stat-sub   { font-size:.68rem;margin-block-start:.1rem; }

/* A chart needs a box whose height does not depend on the chart.
   Every chart here runs with maintainAspectRatio:false, which means Chart.js
   measures the parent on each render and resizes the canvas to fit. When the
   parent's own height comes from its contents — a canvas and, in one case, a
   note beside it — that is a feedback loop: each re-render measures a box the
   last render made taller. It does not show on first load, only after a few
   tab switches, which is exactly how somebody using the page finds it and how
   somebody measuring it once does not. The query-intent chart reached 21,925px.

   The height lives inline on the box, per chart. The canvas fills it and is
   never allowed to decide anything, hence !important — Chart.js writes its own
   inline width and height on every resize. */
.chart-box { position:relative; width:100%; }
.chart-box > canvas { position:absolute; inset:0; width:100% !important; height:100% !important; }

/* A table row is one line unless the cell says otherwise.
   "product-library" in a 120px column broke at its hyphen and wrapped, which
   made EVERY row in that table two lines tall — 66px instead of 45px — and the
   Analytics tables several hundred pixels longer than they needed to be for the
   same ten rows. Nothing in these tables wants to wrap: the cells that hold
   long text already set their own max-width with an ellipsis, and the rest are
   names, counts and dates.

   A cell that genuinely needs to wrap opts back in with .cell-wrap. */
/* Cells WRAP by default.
 *
 * This was `white-space: nowrap` on every cell in the application, which meant
 * a free-text column could never give way and simply grew to its longest value:
 * the Groups table spent 809px of its 1530 on Description alone, and scrolled
 * sideways inside its wrapper on a 1400px screen.
 *
 * Badges and buttons are unaffected — `.badge` and `.btn` carry their own
 * nowrap, so they still never break mid-control. Anything else that must stay
 * on one line (a date column that looks wrong broken, say) can ask for it with
 * the `.nowrap` class below rather than every cell in the product paying for it. */
tbody td { padding:var(--density-row-py) .875rem;vertical-align:middle; }
/* Opt back out, per column, where breaking genuinely reads worse — a date
   column, say. Dates are not centralised (each admin module formats its own),
   so this is applied where one looks wrong rather than swept globally. */
tbody td.nowrap, thead th.nowrap { white-space: nowrap; }

/* Actions columns hold buttons, and a button that drops onto its own line makes
   a row taller for no reason. .btn is already nowrap internally; this keeps the
   row of them together. */
tbody td.actions { white-space: nowrap; }
tbody td.cell-wrap { white-space:normal; }

/* A row's action buttons stay on one line.
   Each icon button is 36px tall by the .btn min-height, so three of them
   wrapping in an 84px-wide Actions column made every row 91px instead of 45 —
   half as many rows on screen, on Companies, Regions, Announcements, Products
   and Release Notes at once. It reads as the table having lost its content.
   The column is allowed to be as wide as its buttons need; the table already
   scrolls inside .table-wrap when the total no longer fits. */
.row-actions { display:flex;gap:.375rem;flex-wrap:nowrap;align-items:center; }

/* Document row actions sit in fixed slots.
   Not every file can be opened in the browser and not every one publishes a
   checksum, so a plain flex row put Download under View on one line and under
   Verify on the next — the icons moved between rows and nothing stayed where
   the eye last found it. A grid of equal columns keeps each action in its own
   place whether or not that row has it. */
/* One fixed-width slot per action, in a fixed order, with an empty span
   where a file has no such action — so Download does not move about the
   row depending on whether that particular file can be viewed in the
   browser or publishes a checksum. Auto columns rather than a count, so
   a caller with three actions needs no override. */
.doc-actions { display:grid;grid-auto-flow:column;grid-auto-columns:30px;gap:.3rem;justify-content:end;align-items:center; }

/* Two adjacent text columns styled identically, with nothing between them,
   read as one run of words — Category and Sub-category on Browse Prices, where
   both hold short phrases and the eye has no boundary to find. A rule and a
   little breathing room, rather than colouring one of them differently: they
   are equals, and the reader only needs to know where one ends. */
.col-split { border-inline-start:1px solid var(--border); }
td.col-split { padding-inline-start:.7rem; }

/* ── Tabs ──────────────────────────────────────────────────── */
.tabs { display:flex;gap:0;border-block-end:2px solid var(--border);overflow-x:auto;overflow-y:hidden; }
.tab-btn { padding:.625rem 1rem;font-size:.9rem;font-weight:500;background:transparent;border:none;color:var(--text-secondary);cursor:pointer;white-space:nowrap;position:relative;font-family:inherit;border-block-end:2px solid transparent;margin-block-end:-2px;transition:color var(--transition); }
.tab-btn:hover { color:var(--text-primary); }
.tab-btn.active { color:var(--accent);border-block-end-color:var(--accent); }
.tab-panel { display:none; }
.tab-panel.active { display:block; }

/* Vertical variant — used by Settings, whose category count (8 and growing)
   reads better as a left-side list than a horizontal bar. Overrides .tabs/
   .tab-btn's row layout; other .tabs users (Legal Pages, Insights, etc.)
   are untouched since this only applies under .settings-nav. */
.settings-nav { flex-direction:column;align-items:stretch;gap:.125rem;border-block-end:none;overflow:visible;flex-shrink:0;inline-size:200px; }
.settings-nav .tab-btn { text-align:start;padding:.5rem .75rem;border-radius:var(--radius-md);border-block-end:none;margin-block-end:0; }
.settings-nav .tab-btn:hover { background:var(--bg-surface-2); }
.settings-nav .tab-btn.active { background:var(--accent-subtle);color:var(--accent-text); }
.settings-nav .settings-nav-label { font-size:.6875rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--text-tertiary);padding:.75rem .75rem .25rem; }
.settings-nav .settings-nav-label:first-child { padding-block-start:.25rem; }

/* ── Modals ────────────────────────────────────────────────── */
.modal-backdrop { position:fixed;inset:0;background:var(--bg-overlay);z-index:600;display:flex;align-items:center;justify-content:center;padding:1rem;animation:fadeIn .15s ease; }
.modal-backdrop[hidden] { display:none !important; }
@keyframes fadeIn { from { opacity:0; } to { opacity:1; } }
@keyframes slideUp { from { transform:translateY(16px);opacity:0; } to { transform:translateY(0);opacity:1; } }
.modal { background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-xl);box-shadow:var(--shadow-xl);width:100%;max-width:520px;max-height:90vh;overflow-y:auto;animation:slideUp .2s ease; }
.modal-lg { max-width:760px; }
.modal-xl { max-width:1000px; }
.modal.modal-expanded { width:96vw;max-width:96vw;height:92vh;max-height:92vh; }
.modal.modal-expanded .ql-editor { min-height:calc(92vh - 340px); }
.modal-header { padding:1.25rem 1.5rem 1rem;display:flex;align-items:flex-start;gap:.75rem;border-block-end:1px solid var(--border);position:sticky;top:0;background:var(--bg-surface);z-index:1; }
.modal-title { font-size:1.0625rem;font-weight:600;flex:1; }
.modal-close { background:none;border:none;cursor:pointer;color:var(--text-tertiary);border-radius:var(--radius-sm);padding:.125rem; }
.modal-close:hover { color:var(--text-primary);background:var(--bg-surface-2); }
.modal-body { padding:1.25rem 1.5rem; }
.modal-footer { padding:1rem 1.5rem;border-block-start:1px solid var(--border);display:flex;gap:.5rem;justify-content:flex-end; }

/* ── Alerts ────────────────────────────────────────────────── */
/* Block, not flex.
   As a flex container every run of text, every <strong> and every <em> inside
   an alert became its own flex item, laid out in a row and wrapped
   independently — so "Announcements display inline on the portal dashboard"
   rendered as scattered words in the wrong order. The leading icon floats so
   it still sits beside the text, whether that text is loose inline content or
   wrapped in a block of its own. */
.alert { padding:.75rem 1rem;border-radius:var(--radius-md);font-size:.9rem;display:block;border:1px solid transparent;margin-block-end:1rem; }
.alert > svg:first-child { float:inline-start;margin-inline-end:.5rem;margin-block-start:.15rem; }
.alert::after { content:'';display:block;clear:both; }
.alert-info    { background:var(--blue-subtle);color:var(--blue);border-color:color-mix(in srgb,var(--blue) 20%,transparent); }
.alert-success { background:var(--green-subtle);color:var(--green);border-color:color-mix(in srgb,var(--green) 20%,transparent); }
.alert-warning { background:var(--amber-subtle);color:var(--amber);border-color:color-mix(in srgb,var(--amber) 20%,transparent); }
.alert-danger  { background:var(--red-subtle);color:var(--red);border-color:color-mix(in srgb,var(--red) 20%,transparent); }

/* ── Breadcrumb ────────────────────────────────────────────── */
.breadcrumb { display:flex;align-items:center;gap:.375rem;font-size:.875rem;flex-wrap:wrap; }
.breadcrumb-item { color:var(--text-tertiary); }
.breadcrumb-item a { color:var(--text-secondary);text-decoration:none; }
.breadcrumb-item a:hover { color:var(--text-primary); }
.breadcrumb-sep { color:var(--text-tertiary); }
.breadcrumb-item.active { color:var(--text-primary);font-weight:500; }

/* ── Pagination ────────────────────────────────────────────── */
.pagination { display:flex;gap:.25rem;align-items:center;flex-wrap:wrap; }
.page-btn { min-width:36px;height:36px;padding:0 .5rem;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-surface);color:var(--text-secondary);cursor:pointer;font-size:.875rem;font-family:inherit;display:flex;align-items:center;justify-content:center;transition:background var(--transition),border-color var(--transition); }
.page-btn:hover { background:var(--bg-surface-2); }
.page-btn.active { background:var(--accent);color:#fff;border-color:var(--accent); }
.page-btn:disabled { opacity:.4;cursor:not-allowed; }

/* ── Document list ─────────────────────────────────────────── */
.doc-icon { width:40px;height:44px;border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;font-family:'DM Mono',monospace;flex-shrink:0;letter-spacing:-.03em;color:#fff; }
.doc-icon-pdf   { background:#e84040; }
.doc-icon-docx  { background:#2b579a; }
.doc-icon-xlsx  { background:#207245; }
.doc-icon-pptx  { background:#d24726; }
.doc-icon-html  { background:#e44d26; }
.doc-icon-other { background:var(--text-tertiary); }
.doc-info { flex:1;min-width:0; }
.doc-title { font-weight:500;font-size:.9375rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; }
.doc-meta { font-size:.8125rem;color:var(--text-tertiary);display:flex;gap:.625rem;flex-wrap:wrap;margin-block-start:.125rem; }

/* ── Folder tree ───────────────────────────────────────────── */
.folder-tree { font-size:.9rem; }
.folder-item { display:flex;align-items:center;gap:.375rem;padding:.3125rem .5rem;border-radius:var(--radius-sm);cursor:pointer;color:var(--text-secondary);transition:background var(--transition),color var(--transition);white-space:nowrap;overflow:hidden;text-overflow:ellipsis; }
.folder-item:hover { background:var(--bg-surface-2);color:var(--text-primary); }
.folder-item.active { background:var(--accent-subtle);color:var(--accent-text);font-weight:500; }
/* The folder tree's only home is the sidebar, and the colours above are for a
   light card: --text-secondary (#4a4b5e) on the sidebar's #1c1f2e is 1.91:1,
   against WCAG AA's 4.5:1. Subfolders were effectively invisible — reported as
   "too hard to read", which understates it. Hover and active were wrong in the
   same way, painting a pale card pill inside a dark panel.

   Uses the sidebar's own tokens rather than fixed colours, so a theme that
   flips the sidebar light stays correct: --text-sidebar is validated against
   that theme's --bg-sidebar (see ThemeContrastValidator). 12.91:1 on the
   default theme. */
.sidebar .folder-item        { color:var(--text-sidebar); }
.sidebar .folder-item:hover  { background:var(--bg-sidebar-item);color:var(--text-sidebar); }
.sidebar .folder-item.active { background:var(--bg-sidebar-item);color:var(--text-sidebar);font-weight:600; }
.folder-children { padding-inline-start:1.25rem; }

/* ── Nav menu tree (drag-and-drop admin editor) ───────────────── */
.nav-tree-list { list-style:none;margin:0;padding:0; }
.nav-tree-children { padding-inline-start:1.75rem;margin-block-start:.375rem; }
.nav-tree-item { margin-block-end:.375rem; }
.nav-tree-row { display:flex;align-items:center;gap:.625rem;padding:.5rem .75rem;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-surface);cursor:grab; }
.nav-tree-row-inactive { opacity:.55; }
.nav-tree-row:active { cursor:grabbing; }
.nav-tree-row.nav-drag-before { box-shadow:inset 0 2px 0 0 var(--accent); }
.nav-tree-row.nav-drag-after  { box-shadow:inset 0 -2px 0 0 var(--accent); }
.nav-tree-row.nav-drag-into   { outline:2px dashed var(--accent);outline-offset:-2px; }
.nav-tree-icon { display:flex;align-items:center;justify-content:center;flex-shrink:0;width:18px;color:var(--text-secondary); }
.nav-tree-icon-auto { color:var(--text-tertiary);opacity:.6; }
/* The badges wrap under the name rather than running off the side: an item
   can carry four of them — kind, visibility, required capability, deleted —
   and none of them can be made narrower. min-width:0 lets the label shrink;
   only flex-wrap stops the row growing past a phone screen. */
.nav-tree-label { flex:1;min-width:0;display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;row-gap:.25rem; }
.nav-tree-root-drop { padding:.625rem;border:1px dashed var(--border);border-radius:var(--radius-md);text-align:center;font-size:.8125rem;color:var(--text-tertiary);margin-block-start:.5rem; }
.nav-tree-root-drop.nav-drag-into { border-color:var(--accent);color:var(--accent-text); }

/* ── AI Chat widget ────────────────────────────────────────── */
.ai-panel { background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-lg);display:flex;flex-direction:column;height:600px;overflow:hidden; }
.ai-panel-header { padding:.875rem 1rem;border-block-end:1px solid var(--border);display:flex;align-items:center;gap:.625rem;background:var(--bg-surface-2); }
.ai-panel-avatar { width:28px;height:28px;background:var(--accent);border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-size:.75rem;font-weight:700;flex-shrink:0; }
.ai-panel-name { font-weight:600;font-size:.9rem;flex:1; }
.ai-panel-disclosure { font-size:.7rem;color:var(--text-tertiary); }
.ai-messages { flex:1;overflow-y:auto;padding:1rem;display:flex;flex-direction:column;gap:1rem;scroll-behavior:smooth; }
.ai-msg { display:flex;gap:.625rem;max-width:100%; }
.ai-msg-user { flex-direction:row-reverse; }
.ai-msg-bubble { max-width:80%;padding:.625rem .875rem;border-radius:var(--radius-md);font-size:.9rem;line-height:1.55; }
.ai-msg-assistant .ai-msg-bubble { background:var(--bg-surface-2);color:var(--text-primary); }
.ai-msg-user .ai-msg-bubble { background:var(--accent);color:#fff; }
.ai-sources { margin-block-start:.5rem; }
.ai-source-chip { display:inline-flex;align-items:center;gap:.25rem;background:var(--bg-surface-3);color:var(--text-secondary);padding:.1875rem .5rem;border-radius:var(--radius-pill);font-size:.75rem;cursor:pointer;margin:.125rem;border:1px solid var(--border); }
.ai-source-chip:hover { background:var(--bg-surface-2);color:var(--text-primary); }
.ai-source-chip svg { width:12px;height:12px;flex-shrink:0;pointer-events:none; }
.ai-typing-dot { display:inline-flex;gap:4px;align-items:center;padding:.5rem; }
.ai-typing-dot span { width:6px;height:6px;border-radius:50%;background:var(--text-tertiary);animation:typingBounce 1.2s ease infinite; }
.ai-typing-dot span:nth-child(2) { animation-delay:.2s; }
.ai-typing-dot span:nth-child(3) { animation-delay:.4s; }
@keyframes typingBounce { 0%,60%,100% { transform:translateY(0); } 30% { transform:translateY(-4px); } }
.ai-input-row { padding:.75rem;border-block-start:1px solid var(--border);display:flex;gap:.5rem;background:var(--bg-surface-2); }
.ai-feedback { display:flex;gap:.5rem;padding:.25rem 0; }
.ai-feedback-btn { background:none;border:none;cursor:pointer;color:var(--text-tertiary);padding:.25rem;border-radius:var(--radius-sm);font-size:.75rem;display:flex;align-items:center;gap:.25rem;font-family:inherit; }
.ai-feedback-btn:hover { color:var(--text-primary);background:var(--bg-surface-3); }
.ai-feedback-btn.active-up   { color:var(--green); }
.ai-feedback-btn.active-down { color:var(--red); }
.ai-feedback-btn svg { width:14px;height:14px;flex-shrink:0;pointer-events:none; }

/* ── Loading states ────────────────────────────────────────── */
.skeleton { background:linear-gradient(90deg,var(--bg-surface-2) 25%,var(--bg-surface-3) 50%,var(--bg-surface-2) 75%);background-size:200% 100%;animation:shimmer 1.5s infinite;border-radius:var(--radius-sm); }
@keyframes shimmer { to { background-position:-200% 0; } }
.spinner { width:20px;height:20px;border:2px solid var(--border);border-block-start-color:var(--accent);border-radius:50%;animation:spin .7s linear infinite; }
@keyframes spin { to { transform:rotate(360deg); } }

/* ── Theme / density toggle ────────────────────────────────── */
/* Density is a separate personal preference from light/dark (see Density in
   core.js) but reads as the same sidebar-footer icon-button-group, so it
   shares this exact CSS rather than duplicating it under a new name. */
/* 24px, not 28. Two of these groups at 28px came to 200px against 185px of
   usable sidebar width, so they wrapped onto separate lines and the footer
   stood three rows tall — taking that height off the menu, which is the part
   that scrolls. At 24px they sit on one line. */
.theme-toggle,.density-toggle { display:flex;gap:.2rem;background:var(--bg-surface-3);border-radius:var(--radius-pill);padding:2px;flex:0 0 auto; }
.theme-btn,.density-btn { width:24px;height:24px;border:none;border-radius:var(--radius-pill);background:transparent;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--text-tertiary);transition:background var(--transition),color var(--transition); }
.theme-btn.active,.density-btn.active { background:var(--bg-surface);color:var(--text-primary);box-shadow:var(--shadow-sm); }

/* ── Empty state ───────────────────────────────────────────── */
.empty-state { text-align:center;padding:3rem 1.5rem;color:var(--text-secondary); }
.empty-state-icon { font-size:2.5rem;margin-block-end:.75rem;opacity:.4; }
.empty-state h3 { font-size:1rem;font-weight:600;margin-block-end:.375rem;color:var(--text-primary); }
.empty-state p { font-size:.9rem;max-width:320px;margin:0 auto 1rem; }

/* ── Dropdown ──────────────────────────────────────────────── */
.dropdown { position:relative;display:inline-block; }
.dropdown-menu { position:absolute;inset-block-start:calc(100% + 4px);inset-inline-end:0;min-width:180px;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:var(--shadow-lg);z-index:300;padding:.25rem;animation:slideUp .12s ease; }
.dropdown-item { display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-radius:var(--radius-sm);font-size:.9rem;color:var(--text-secondary);cursor:pointer;background:none;border:none;width:100%;text-align:start;font-family:inherit;transition:background var(--transition),color var(--transition); }
.dropdown-item:hover { background:var(--bg-surface-2);color:var(--text-primary); }
.dropdown-item.danger:hover { background:var(--red-subtle);color:var(--red); }
.dropdown-divider { height:1px;background:var(--border);margin:.25rem 0; }

/* ── Admin layout specifics ────────────────────────────────── */
.admin-toolbar { display:flex;align-items:center;gap:.75rem;padding-block-end:1rem;flex-wrap:wrap; }
.admin-toolbar-start { flex:1;display:flex;align-items:center;gap:.5rem;flex-wrap:wrap; }
.admin-toolbar-end   { display:flex;align-items:center;gap:.5rem; }
.stats-grid { display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:1rem;margin-block-end:1.5rem; }

/* Two panels in one card, with a rule between them — and one column when there
   is no room for two. Declared one-column FIRST so the phone is the default
   case: `1fr 1fr` written flat has a min-content minimum per track, so on a
   393px screen the pair stayed side by side and pushed the page out instead.
   The divider belongs to the side-by-side case and is drawn with it. */
.split-2 { display:grid;grid-template-columns:1fr; }
@media (min-width:560px) {
  .split-2 { grid-template-columns:1fr 1fr; }
  .split-2 > :first-child { border-inline-end:1px solid var(--border); }
}
.stat-card { background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1rem 1.25rem; }
.stat-label { font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-secondary); }
.stat-value { font-size:1.75rem;font-weight:600;line-height:1.2;margin-block:.25rem; }
.stat-sub   { font-size:.73rem;color:var(--text-tertiary); }
.stat-delta { font-size:.8125rem;color:var(--text-tertiary); }

/* ── Responsive ────────────────────────────────────────────── */
@media (max-width:900px) {
  .portal-layout { grid-template-columns:minmax(0,1fr); }
  .portal-layout .video-panel { display:none; }
}

@media (max-width:768px) {
  :root { --sidebar-w:0px; }
  .sidebar { transform:translateX(-100%);width:260px;box-shadow:var(--shadow-xl); }
  .sidebar.open { transform:translateX(0); }
  [dir="rtl"] .sidebar { transform:translateX(100%); }
  [dir="rtl"] .sidebar.open { transform:translateX(0); }
  .main-content { margin-inline-start:0; }
  .page-body { padding:1rem; }
  .admin-toolbar { flex-direction:column;align-items:stretch; }
  .admin-toolbar-end { justify-content:flex-end; }
  .modal { max-width:100%;margin:0;border-radius:var(--radius-lg) var(--radius-lg) 0 0;max-height:95vh; }
  .modal-backdrop { align-items:flex-end;padding:0; }
  .drawer,.drawer-lg { width:100vw; }
  .ai-panel { height:400px; }
  table { font-size:.8125rem; }
  thead th,tbody td { padding:.5rem .625rem; }
}

/* The hamburger that opens the off-canvas sidebar.
   Visibility in CSS rather than JavaScript, deliberately: the pages that
   predate this rule flip an inline display from script, and an inline style
   wins, so they are unaffected — but a page added from now on gets working
   navigation on a phone by including the button, with nothing to remember to
   wire up. Quote Builder and Quotes shipped without either, which left their
   sidebar parked at x=-260 with 28 links in it and no way to reach them. */
.sidebar-toggle { display:none; }
@media (max-width:768px) { .sidebar-toggle { display:inline-flex; } }

.sidebar-overlay { display:none;position:fixed;inset:0;background:var(--bg-overlay);z-index:190; }
@media (max-width:768px) { .sidebar-overlay.open { display:block; } }

/* ── RTL ───────────────────────────────────────────────────── */
[dir="rtl"] .sidebar { inset-inline-start:auto;inset-inline-end:0; }
[dir="rtl"] .main-content { margin-inline-start:0;margin-inline-end:var(--sidebar-w); }
[dir="rtl"] .nav-item.active::before { inset-inline-start:auto;inset-inline-end:0;border-radius:2px 0 0 2px; }
[dir="rtl"] select.form-control { background-position:left .75rem center;padding-inline-start:2rem;padding-inline-end:.75rem; }
[dir="rtl"] .drawer { inset-inline-end:auto;inset-inline-start:-860px; }
[dir="rtl"] .drawer.open { inset-inline-start:0; }

/* ── Quill editor overrides ────────────────────────────────── */
.ql-toolbar { border-color:var(--border)!important;border-radius:var(--radius-md) var(--radius-md) 0 0!important;background:var(--bg-surface-2)!important; }
.ql-container { border-color:var(--border)!important;border-radius:0 0 var(--radius-md) var(--radius-md)!important;font-family:var(--font-body)!important;font-size:.9375rem!important; }
.ql-editor { min-height:180px;color:var(--text-primary)!important; }
.ql-toolbar .ql-stroke { stroke:var(--text-secondary)!important; }
.ql-toolbar .ql-fill { fill:var(--text-secondary)!important; }
.ql-toolbar button:hover .ql-stroke { stroke:var(--accent)!important; }
[data-theme="dark"] .ql-container { background:var(--bg-surface)!important; }
[data-theme="dark"] .ql-editor { background:var(--bg-surface)!important; }

/* ── Print ─────────────────────────────────────────────────── */
@media print {
  .sidebar,.page-header,.cookie-notice,.impersonation-banner,.ai-panel,.btn { display:none!important; }
  .main-content { margin:0; }
}

/* ── Login page ────────────────────────────────────────────── */
.login-page { min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--bg-page);padding:1.5rem; }
.login-card { width:100%;max-width:420px;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-xl);box-shadow:var(--shadow-xl);overflow:hidden; }
.login-header { background:var(--bg-sidebar);padding:2rem;text-align:center;display:flex;flex-direction:column;align-items:center;gap:.75rem; }
.login-logo { height:48px;width:auto; }

/* ── Alternative sign-in methods ──
   Below the primary form, never beside it: the password field is the path most
   people take and a row of equal-weight buttons makes every option look like a
   decision. */
.alt-divider {
  display:flex;align-items:center;gap:.75rem;margin:1.25rem 0 .9rem;
  color:var(--text-tertiary);font-size:.75rem;
}
.alt-divider::before, .alt-divider::after {
  content:"";flex:1;height:1px;background:var(--border);
}
.alt-provider-buttons { display:flex;flex-direction:column;gap:.5rem; }
.alt-provider {
  width:100%;display:flex;align-items:center;justify-content:center;gap:.6rem;
}
.alt-provider svg { flex-shrink:0; }

/* ── Legal links ──
   Two places, one look: the sign-in footer and the portal sidebar. Small and
   quiet — they must be findable without competing with anything. */
.legal-links a, .sidebar-legal a { color:var(--text-tertiary);text-decoration:none; }
.legal-links a:hover, .sidebar-legal a:hover { color:var(--text-secondary);text-decoration:underline; }
.legal-links { display:flex;flex-wrap:wrap;gap:.25rem;align-items:center; }
/* One row for the version and the legal links together. Wraps only when the
   sidebar is genuinely too narrow for them, rather than always taking two. */
/* One line, and the numbers it takes to get there.
   The sidebar is 240px; this box has 200px and its padding eats into that. The
   content is "v2026.9.118 · Privacy · Cookies · Terms", which at 11px measures
   195px against a 176px content box — 19px short, which is why two earlier
   attempts at flex rules changed nothing. Three levers, measured:
     padding  12px -> 8px each side   content box 176 -> 184
     font     11px -> 10px            content      195 -> 177
     gap      6.4px -> 4.8px          content      177 -> 175
   175 into 184. 10px is small, but this is legal smallprint and the links carry
   their own titles.

   The ceiling, measured: a version string of about twelve characters is the
   most that fits. "v2026.9.118" does; "v2026.12.9999" does not, and calendar
   versioning means one day it will not. So wrapping stays ALLOWED and the
   line-height is tightened — two rows then cost 30px rather than 43, instead of
   the links being clipped or the version being truncated. The version is the
   first thing support asks for and must never be shortened to fit a line. */
.sidebar-smallprint {
  display:flex;align-items:center;flex-wrap:wrap;gap:.15rem .3rem;
  padding:.5rem .5rem 0;font-size:.625rem;line-height:1.35;
}
/* The version carries flex:0 0 100% from its old life as its own row in the
   footer — which is what kept it on a line of its own here. Inside the
   smallprint row it sizes to its content like everything else. */
.sidebar-smallprint .sidebar-version { flex:0 1 auto;padding:0;margin:0;text-align:start; }
.sidebar-smallprint .sidebar-version::after { content:" ·";opacity:.6; }
.sidebar-legal {
  font-size:inherit;line-height:1.35;
  color:var(--text-sidebar-2);display:flex;flex-wrap:wrap;gap:.2rem;
}
.sidebar-legal a { color:var(--text-sidebar-2); }
.sidebar-legal a:hover { color:var(--text-sidebar); }
.login-title { color:var(--text-sidebar);font-size:1.125rem;font-weight:600; }
.login-body { padding:1.75rem; }

/* Rendered rich-text HTML body — a Page's content (view-page in portal.html),
   originally pages.html's own scoped styles before Pages moved into the main
   dashboard shell so a page reads as part of the site, not a separate tab. */
.page-content { line-height:1.75; color:var(--text-secondary); }
.page-content h1, .page-content h2, .page-content h3 { color:var(--text-primary); margin-block:1.25rem .5rem; }
.page-content p  { margin-block:.75rem; }
.page-content ul, .page-content ol { margin-block:.75rem; padding-inline-start:1.5rem; }
.page-content img { border-radius:8px; }

/* Row hover.
   These were a matched pair of onmouseover/onmouseout attributes setting
   style.background directly — inline script, and the reason the Content
   Security Policy had to permit it. A hover state was never script's job. */
[data-hover] { transition: background-color .12s ease; }
[data-hover="surface-2"]:hover { background: var(--bg-surface-2); }
[data-hover="surface-3"]:hover { background: var(--bg-surface-3); }


/* Columns worth dropping on a phone.
   The Price Book put Category and Sub-category ahead of the price, so on a
   375px screen the Price column began 584px in — more than a full screen width
   of sideways scrolling before a price book showed a price. Both are already
   filters at the top of the page, so on a phone they are the two columns
   worth losing. */
@media (max-width: 768px) {
  .col-secondary { display: none; }
}

/* The Price Book as cards on a phone.
   Dropping two columns brought the price from 584px to 341px, which is still
   a sideways swipe to read the one number the page exists to show. A table
   cannot be made to fit; laid out as cards there is nothing to scroll.

   Done in CSS against the existing markup rather than by rendering different
   HTML, so there is one renderer to keep correct and the desktop table is
   untouched. */
@media (max-width: 768px) {
  .pricing-table thead { display: none; }

  .pricing-table,
  .pricing-table tbody,
  .pricing-table tr { display: block; width: 100%; }

  .pricing-table tr {
    position: relative;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    margin-block-end: .5rem;
    padding: .625rem .75rem;
    /* Room for the actions, which sit in the top corner. */
    padding-inline-end: 4.25rem;
    background: var(--bg-surface);
  }

  .pricing-table td {
    display: block;
    border: none;
    padding: 0;
    max-width: none !important;
  }

  /* Part number first and prominent — it is what people search by. */
  .pricing-table td:first-child { font-weight: 600; margin-block-end: .15rem; }

  .pricing-table td .price-cell,
  .pricing-table td.price-cell {
    text-align: start;
    font-weight: 600;
    font-size: 1rem;
  }
  .pricing-table td.price-cell { margin-block-start: .35rem; }

  /* The description can breathe: two clamped lines in a 320px cell was a
     desktop constraint that no longer applies. */
  .pricing-table td:nth-child(2) span {
    -webkit-line-clamp: 3 !important;
    font-size: .8125rem;
    color: var(--text-secondary);
  }

  /* Bookmark and temp-list, out of the text flow. */
  .pricing-table td:last-child {
    position: absolute;
    inset-block-start: .5rem;
    inset-inline-end: .5rem;
    width: auto !important;
    text-align: end;
  }

  /* The PRICE BOOK's wrapper no longer has anything to scroll — its table is
     cards now. Scoped with :has() rather than written as `.table-wrap`, which
     is what it said until 2026-09-21 and which turned the scroller off for
     EVERY table on the site at phone width. Only the price book becomes cards;
     the other thirty-odd tables stayed tables, lost the thing that let them
     scroll, and pushed the whole page sideways instead — the admin Users table
     by 655px, Groups by 1069. */
  .table-wrap:has(.pricing-table) { overflow-x: visible; }
}


/* Dense admin tables.
   The Users table carries twelve columns and a filter row. On a 1280px laptop
   the content area is about 975px and the table wanted 1313 — a 338px
   horizontal scrollbar on the page administrators use most. Padding alone was
   336px of it (14px either side of twelve cells), and the filter selects were
   sized by their longest option rather than their column.

   Nothing is removed at normal widths; the table is simply allowed to be the
   width of its contents rather than the width of its padding. */
.table-dense th,
.table-dense td { padding-inline: 8px; }

.table-dense .filter-input { width: 100%; min-width: 0; }
.table-dense select.filter-input { max-width: 110px; }

/* Below a laptop's width, two columns step aside. Both answer questions asked
   occasionally rather than scanned down a list, and both are on the user's own
   edit screen. */
@media (max-width: 1400px) {
  .col-tertiary { display: none; }
}

/* The per-title translate control on a video card.
   Deliberately tiny and quiet: it only exists when the portal is in another
   language, and the video panel is a narrow column where a labelled button on
   every card would cost a line of height each. Grows to full contrast on hover
   and stays highlighted while a title is showing translated. */
.video-translate-btn {
  background:none;border:1px solid transparent;border-radius:3px;cursor:pointer;
  color:var(--text-tertiary);padding:.2rem;flex-shrink:0;align-self:center;
  line-height:1;display:inline-flex;align-items:center;
}
.video-translate-btn:hover        { color:var(--text-secondary);border-color:var(--border); }
.video-translate-btn.active       { color:var(--accent);border-color:var(--border-strong); }
.video-translate-btn:disabled     { opacity:.5;cursor:default; }

/* The star that bookmarks a catalogue item from inside the quote builder.
   Quiet until it means something: an outline you can see but not notice, filled
   and coloured once the item is starred. */
.qb-star {
  background:none;border:none;cursor:pointer;padding:.1rem .15rem;line-height:1;
  color:var(--text-tertiary);display:inline-flex;align-items:center;border-radius:3px;
}
.qb-star svg  { fill:none;stroke:currentColor; }
.qb-star:hover{ color:var(--text-secondary);background:var(--bg-surface-2); }
.qb-star.on   { color:var(--accent); }
.qb-star.on svg { fill:currentColor; }

/* Says when a search has left the section you were browsing. */
.qb-items-scope {
  font-size:.75rem;color:var(--text-secondary);background:var(--bg-surface-2);
  border:1px solid var(--border);border-radius:var(--radius-sm);
  padding:.3rem .5rem;margin-block-end:.4rem;
}

/* A link inside a paragraph has to be findable without seeing colour.
   Roughly one man in twelve cannot reliably tell the accent red from the body
   grey around it, and in author-written content — an announcement, a portal
   page, a legal document — a link is often the only way to the thing being
   announced. Colour alone is a WCAG failure (1.4.1) and axe reports it as
   serious; an underline is the oldest fix there is and costs nothing.

   Scoped to CONTENT, not to the whole product: navigation, buttons and table
   actions are distinguishable by their position and shape, and underlining
   every one of them would make the interface unreadable. */
.announcement-body a,
#view-page-body a,
.page-content a,
.legal-content a {
  text-decoration: underline;
  text-underline-offset: 2px;
}


/* A home-page section heading that is also the way into that section.
   Announcements, Videos and Upcoming training each have a full page of their
   own, and the heading is the thing people try to click first.

   It inherits the heading's own colour and weight rather than looking like a
   link, because a row of blue underlined headings would read as navigation
   rather than as the labels of what is underneath. The affordance appears on
   hover and, importantly, on keyboard focus — a link with no visible focus
   state is unusable for anybody tabbing through the page. */
.home-section-link {
  color: inherit;
  text-decoration: none;
}
.home-section-link:hover,
.home-section-link:focus-visible {
  color: var(--accent-text, inherit);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.home-section-link:focus-visible {
  outline: 2px solid var(--accent-text, currentColor);
  outline-offset: 3px;
  border-radius: 2px;
}

/* ── Collapsed navigation ──────────────────────────────────────────────────
   The quoting screens are the tight ones — a price book with discount columns,
   a quote builder with a products panel beside the lines — and the sidebar is
   225px of that width on every page.

   The whole thing is one variable. `.sidebar` takes its width from
   --sidebar-w and `.main-content` takes its margin from the same, so
   narrowing it here reflows every page at once, with no page needing to know
   this feature exists.

   A RAIL, not a disappearance: the icons stay, so the navigation is still
   there and still clickable. Hiding it outright would trade one annoyance for
   a worse one.

   Desktop only. Below 768px the sidebar is already an off-canvas overlay with
   --sidebar-w:0, so there is nothing to reclaim and a second collapsed state
   would only fight the hamburger. */
@media (min-width:769px) {
  html.nav-collapsed { --sidebar-w:64px; }

  /* Anything that is words rather than a symbol. The labels are hidden rather
     than shrunk because a clipped word is harder to read than no word at all,
     and every item keeps an accessible name — core.js copies the text into
     aria-label/title as it collapses, so a screen reader and a hover tooltip
     both still say what each icon is. */
  html.nav-collapsed .sidebar-logo-text,
  html.nav-collapsed .sidebar-user-info,
  html.nav-collapsed .sidebar-section-label,
  html.nav-collapsed .nav-item > span,
  html.nav-collapsed .nav-item-chevron,
  html.nav-collapsed .sidebar-footer-btn span,
  html.nav-collapsed .theme-toggle,
  html.nav-collapsed .density-toggle { display:none !important; }

  /* A submenu cannot open into 64px. Clicking a group expands the sidebar
     first — see core.js — so this only hides what is already closed. */
  html.nav-collapsed .nav-sub { display:none !important; }

  html.nav-collapsed .sidebar-logo,
  html.nav-collapsed .sidebar-user,
  html.nav-collapsed .sidebar-footer { justify-content:center;padding-inline:.5rem; }
  html.nav-collapsed .nav-item {
    justify-content:center;padding-inline:.5rem;gap:0;
  }
  html.nav-collapsed .sidebar-avatar { margin:0; }

  /* Reading direction decides which way it points, so the arrow means "away
     from the content" in Arabic as much as in English. */
  .nav-collapse-btn svg { transition:transform var(--transition); }
  html.nav-collapsed .nav-collapse-btn svg { transform:rotate(180deg); }
}

/* The collapse control, on the boundary it moves.
   Level with the logo, flush to the inside of the sidebar's right edge —
   .sidebar is overflow:hidden for the width transition, so a button
   straddling the edge would be clipped in half.
   Invisible until wanted: it appears on hover anywhere in the sidebar, and on
   keyboard focus, which matters more — a control that only exists on hover is
   a control a keyboard cannot find. Once collapsed it stays visible, because
   otherwise the way back is something you have to already know. */
.nav-collapse-btn {
  position:absolute;inset-block-start:calc(var(--header-h, 56px) / 2 - 12px);inset-inline-end:8px;
  width:24px;height:24px;display:flex;align-items:center;justify-content:center;
  padding:0;border:none;border-radius:50%;cursor:pointer;
  background:var(--bg-sidebar-hover, rgba(255,255,255,.1));color:var(--text-sidebar-2);
  opacity:0;transition:opacity var(--transition),background var(--transition),color var(--transition);
  z-index:2;
}
.sidebar:hover .nav-collapse-btn,
.nav-collapse-btn:focus-visible,
html.nav-collapsed .nav-collapse-btn { opacity:1; }
.nav-collapse-btn:hover { background:var(--accent);color:#fff; }
.nav-collapse-btn:focus-visible { outline:2px solid var(--accent);outline-offset:2px; }
/* Collapsed, the logo row is only 64px wide — centre it rather than have it
   sit on top of the mark. */
html.nav-collapsed .nav-collapse-btn { inset-inline-end:50%;transform:translateX(50%);inset-block-start:4px; }
/* Pointless on a phone, where the sidebar is an overlay that is already
   hidden by default. */
@media (max-width:768px) { .nav-collapse-btn { display:none; } }

/* A view whose code is still arriving.
   Only ever seen on a slow or cold connection — portal-views.js waits 150ms
   before adding this, because these files are small and same-origin and a
   spinner that flickers for eight milliseconds reads as a glitch rather than
   as progress. */
body.view-loading { cursor: progress; }
body.view-loading::after {
  content: '';
  position: fixed; inset-block-start: 0; inset-inline: 0; height: 2px;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  background-size: 40% 100%;
  animation: view-loading-sweep 900ms linear infinite;
  z-index: 9999;
}
@keyframes view-loading-sweep {
  from { background-position: -40% 0; }
  to   { background-position: 140% 0; }
}
@media (prefers-reduced-motion: reduce) {
  body.view-loading::after { animation: none; background: var(--accent); opacity: .5; }
}

/* ── Legal pages ──────────────────────────────────────────────────────────
   Shared by the two places the policies are read: signed in, as a view inside
   the portal shell, and signed out, on legal.html — which the sign-in footer
   links to and which somebody may well arrive at from an email.

   One set of rules rather than two, because they were two: the standalone page
   carried its own copy, and the moment a second one existed they would have
   started drifting the way three hardcoded link lists already had. */
.legal-tabs {
  display: flex; flex-wrap: wrap; gap: .25rem;
  margin-block-end: 1.5rem;
  border-block-end: 1px solid var(--border);
  padding-block-end: .5rem;
}
.legal-tab {
  padding: .4rem .875rem; border-radius: 6px;
  font: inherit; font-size: .875rem; font-weight: 500;
  cursor: pointer; border: none; background: transparent;
  color: var(--text-secondary); transition: background .15s, color .15s;
}
.legal-tab:hover { background: var(--bg-surface-2); color: var(--text-primary); }
.legal-tab.active { background: var(--accent); color: #fff; font-weight: 600; }
/* Keyboard users get the same ring every other control has: these are the
   only navigation on a page somebody may be reading for legal reasons. */
.legal-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.legal-card {
  background: var(--bg-surface); border-radius: 12px;
  border: 1px solid var(--border); padding: 2rem 2.5rem;
}
.legal-card > h1 { font-size: 1.375rem; font-weight: 700; margin: 0 0 .5rem; color: var(--text-primary); }
.legal-meta { font-size: .8rem; color: var(--text-tertiary); margin-block-end: 1.5rem; }

/* A policy is long-form prose, so it gets long-form measure and rhythm rather
   than the app's default line height — these are read, not scanned. */
.legal-content { line-height: 1.75; color: var(--text-secondary); max-width: 72ch; }
.legal-content h1, .legal-content h2, .legal-content h3 {
  color: var(--text-primary); margin-block: 1.5rem .5rem; line-height: 1.3;
}
.legal-content h2 { font-size: 1.05rem; }
.legal-content h3 { font-size: .95rem; }
.legal-content p  { margin-block: .75rem; }
.legal-content ul, .legal-content ol { margin-block: .75rem; padding-inline-start: 1.5rem; }
.legal-content li { margin-block: .25rem; }
.legal-content a  { color: var(--accent); }
/* A policy that arrives with a wide table must not push the page sideways. */
.legal-content table { display: block; overflow-x: auto; max-width: 100%; border-collapse: collapse; }
.legal-content th, .legal-content td { border: 1px solid var(--border); padding: .4rem .6rem; text-align: start; }

.legal-empty { text-align: center; padding: 3rem; color: var(--text-tertiary); }

@media (max-width: 600px) {
  .legal-card { padding: 1.25rem; }
}
}

/* ── A table cell that clips instead of bleeding into the next column ──────
 *
 * `table-layout:fixed` shares the width out by the column widths rather than
 * by the longest value, which is what stops one long description deciding the
 * width of the whole table. What it does NOT do is stop the text itself
 * overflowing its cell — so a long category ran straight across the columns
 * beside it and the row became unreadable.
 *
 * Clipped in CSS rather than shortened in JavaScript, deliberately: the whole
 * string stays in the document, so a screen reader reads all of it, and
 * core.js's truncation rule gives it a tooltip when — and only when — it is
 * genuinely cut off. Text sliced in script is gone before it reaches the DOM
 * and neither can recover it. */
.cell-clip {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 0;   /* fixed layout needs this for the cell to clip at all */
}

/* A cell holding two stacked lines — a name with the address under it — clips
   each line, not the cell. core.js only offers a truncation tooltip to an
   element that holds text of its OWN, so a td with children inside would clip
   silently with no way to read what was cut. */
.cell-clip > div { overflow: hidden; text-overflow: ellipsis; }

/* The quieter second line of a two-line cell — an address under a name, a
   certificate number under its topic, a start date under an end date. */
.cell-sub { font-size:.75rem;color:var(--text-tertiary);overflow:hidden;text-overflow:ellipsis; }

/* ── Which quote screen you are on ─────────────────────────────────────────────
   One segmented control, shared by the list view and the search view, so the
   pair reads as a switch between two views of the same thing rather than as
   two links that happen to swap places. Hidden when only one is on offer. */
/* Drawn like the currency switch beside it (CAD / EUR / GBP / USD), so the
   header has one kind of toggle, not two. The other view is a link, and the
   site-wide a:hover underline is cancelled here — it made it read as a link. */
.qb-views, .qc-views {
  display:inline-flex; gap:2px; padding:2px; border-radius:6px;
  background:var(--bg-surface-2); flex-shrink:0;
}
.qb-view, .qc-view {
  padding:.2rem .5rem; border-radius:4px; font-size:.72rem; font-weight:700;
  letter-spacing:.02em; text-decoration:none; color:var(--text-tertiary); white-space:nowrap;
}
.qb-view:hover, .qc-view:hover     { color:var(--text-primary); text-decoration:none; }
.qb-view.current, .qc-view.current { background:var(--bg-surface); color:#1a2942; box-shadow:0 1px 3px rgba(0,0,0,.1); cursor:default; }

/* Bookmarks switched off for this person (core.js): hide every star. */
.no-bookmarks [data-act="bookmarkItem"],
.no-bookmarks [data-act="toggleBookmark"],
.no-bookmarks [data-act="qcToggleBookmark"],
.no-bookmarks [data-act="toggleQbBookmark"],
.no-bookmarks [data-act="toggleBookmarkFilter"],
.no-bookmarks #iframe-bookmark { display: none !important; }

/* Role editor › Capabilities: every right, ticked where the role holds it. */
.g-cap-list { max-height:calc(100vh - 22rem); min-height:16rem; overflow:auto; margin-block-end:.75rem; padding-inline-end:.25rem; }
.g-cap-area { border:1px solid var(--border); border-radius:var(--radius-md); padding:.25rem .75rem .5rem; margin:0 0 .75rem; }
.g-cap-area legend { font-size:.75rem; font-weight:700; text-transform:uppercase; letter-spacing:.04em; color:var(--text-secondary); padding:0 .35rem; }
.g-cap-area-count { font-weight:500; text-transform:none; letter-spacing:0; color:var(--accent-text); margin-inline-start:.35rem; }
.g-cap-row { display:flex; align-items:center; justify-content:space-between; gap:.75rem; padding:.4rem .25rem; border-block-end:1px solid var(--border-subtle, var(--border)); }
.g-cap-row:last-child { border-block-end:none; }
.g-cap-row.is-on { background:var(--bg-surface-2); }
.g-cap-row label { display:flex; align-items:flex-start; gap:.6rem; cursor:pointer; flex:1; min-width:0; font-size:.875rem; }
.g-cap-row input[type=checkbox] { margin-block-start:.2rem; flex-shrink:0; }
.g-cap-desc { display:block; font-size:.78rem; color:var(--text-secondary); margin-block-start:.1rem; }
.g-cap-row select { width:auto; padding:.15rem .4rem; font-size:.75rem; flex-shrink:0; }

/* ── The quote grid both quote screens share (js/quote-lines.js) ─────────────── */
.qty-input-sm {
  width:52px; text-align:center;
  border:1.5px solid var(--border); border-radius:6px;
  padding:.2rem .25rem; font-size:.875rem; font-weight:500;
  background:var(--bg-surface); color:var(--text-primary); transition:all .15s;
  -moz-appearance:textfield; appearance:textfield;
}
.qty-input-sm:focus { outline:none; border-color:#c8102e; box-shadow:0 0 0 3px rgba(200,16,46,.1); }
.qty-input-sm.has-qty { border-color:var(--success-text); background:#f0fdf4; font-weight:700; color:#15803d; }
html[data-theme="dark"] .qty-input-sm.has-qty { background:#052e16; color:#4ade80; }
/* No spinner arrows (appearance:textfield above): the browser draws them inside
   the box on the right, so a "centred" number sat visibly left of centre (Paul,
   2026-10-06). Typing is how these are used; click-to-add still works. */
.qty-input-sm::-webkit-outer-spin-button,
.qty-input-sm::-webkit-inner-spin-button { -webkit-appearance:none; margin:0; }
.ql-wrap      { display:flex; flex-direction:column; min-height:0; }
.ql-scroll    { overflow-x:auto; }
.ql-grid      { width:100%; border-collapse:collapse; font-size:.8125rem; }
.ql-grid th   { text-align:start; font-size:.7rem; font-weight:600; color:var(--text-tertiary);
                text-transform:uppercase; letter-spacing:.03em; padding:.45rem .5rem;
                border-block-end:1px solid var(--border); white-space:nowrap; position:sticky; inset-block-start:0;
                background:var(--bg-surface); z-index:1; }
.ql-grid td   { padding:.35rem .5rem; border-block-end:1px solid var(--border); vertical-align:middle; }
.ql-num       { text-align:end !important; white-space:nowrap; }
.ql-qty       { width:4.5rem; }
.ql-act       { width:1%; white-space:nowrap; text-align:end; }
.ql-item      { min-width:10rem; }
.ql-part      { font-family:monospace; font-weight:600; color:var(--accent-text); margin-inline-end:.4rem; }
.ql-desc      { color:var(--text-primary); }
.ql-cat       { display:block; font-size:.7rem; color:var(--text-tertiary); }
.ql-yours     { color:var(--success-text); font-weight:600; }
/* A discount % under the price it produced, smaller, WITHOUT moving the price:
   it hangs just below the figure, so the price stays on the same line as the
   row's other figures (Paul, 2026-10-06). Both quote screens use it. */
.price-hang   { position:relative; display:inline-block; }
.price-off    { position:absolute; inset-inline-end:0; inset-block-start:100%; margin-block-start:.1rem;
                font-size:.62rem; line-height:1; font-weight:500; color:var(--text-tertiary); white-space:nowrap; }
.ql-total     { font-weight:600; }
.ql-qty-ro    { display:inline-block; min-width:2rem; text-align:center; font-weight:600; }
.ql-disc      { width:4.5rem !important; text-align:end; }
.ql-empty, .ql-empty-section { color:var(--text-tertiary); text-align:center; padding:1.5rem .5rem !important; }
.ql-empty-section { padding:.6rem .5rem !important; font-size:.78rem; }
.ql-icon      { background:none; border:1px solid transparent; border-radius:4px; padding:.15rem .4rem;
                font-size:.75rem; color:var(--text-secondary); cursor:pointer; line-height:1.2; }
.ql-icon:hover:not(:disabled) { border-color:var(--border); color:var(--text-primary); background:var(--bg-surface-2); }
.ql-icon:disabled { opacity:.35; cursor:default; }
.ql-danger:hover:not(:disabled) { color:var(--danger-text, #c8102e); }
.ql-move      { font-size:.72rem; padding:.1rem .2rem; border:1px solid var(--border); border-radius:4px;
                background:var(--bg-surface); color:var(--text-secondary); max-width:5.5rem; }

/* A section heading row: its name (typed over in place), where new items go,
   and its tools. The active one is marked, because that is where the next
   search result, bookmark or pasted list lands. */
.ql-section td { background:var(--bg-surface-2); border-block-start:2px solid var(--border); padding:.4rem .5rem; cursor:pointer; }
.ql-section.active td { border-block-start-color:#c8102e; background:color-mix(in srgb, #c8102e 6%, var(--bg-surface)); }
.ql-sect-row  { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap; }
.ql-sect-name { font-weight:700; font-size:.875rem; color:var(--text-primary); background:transparent;
                border:1px solid transparent; border-radius:4px; padding:.15rem .3rem; min-width:8rem; flex:1 1 12rem; max-width:24rem; }
input.ql-sect-name:hover { border-color:var(--border); }
input.ql-sect-name:focus { border-color:#c8102e; outline:none; background:var(--bg-surface); }
.ql-here      { font-size:.65rem; font-weight:700; text-transform:uppercase; letter-spacing:.04em;
                color:var(--text-tertiary); border:1px solid var(--border); border-radius:999px; padding:.05rem .45rem; white-space:nowrap; }
.ql-section.active .ql-here { color:#fff; background:#c8102e; border-color:#c8102e; }
.ql-sect-tools{ display:flex; align-items:center; gap:.2rem; margin-inline-start:auto; flex-wrap:wrap; cursor:default; }
.ql-sect-disc { font-size:.72rem; color:var(--text-secondary); display:inline-flex; align-items:center; gap:.25rem; margin-inline-end:.3rem; }
.ql-sect-disc input { width:4rem; font-size:.75rem; padding:.1rem .3rem; border:1px solid var(--border); border-radius:4px; }
.ql-subtotal td { font-size:.78rem; color:var(--text-secondary); text-align:end; border-block-end:none; padding-block-end:.6rem; }
.ql-subtotal .ql-num { color:var(--text-primary); font-weight:700; }
.ql-foot      { display:flex; align-items:center; gap:1rem; flex-wrap:wrap; padding:.7rem .5rem; border-block-start:2px solid var(--border); }
.ql-totals    { margin-inline-start:auto; display:flex; align-items:baseline; gap:1rem; flex-wrap:wrap; font-size:.8125rem; color:var(--text-secondary); }
.ql-save      { color:var(--success-text); }
.ql-grand     { font-size:.9rem; color:var(--text-primary); }
.ql-grand strong { font-size:1.05rem; }

/* List screen: which section the quantities typed while browsing go into. */
.qb-add-to    { font-size:.75rem; color:var(--text-secondary); white-space:nowrap; }
.qb-add-to select { font-size:.75rem; margin-inline-start:.3rem; padding:.15rem .3rem; border:1px solid var(--border); border-radius:4px; }

/* ── The quote screens' header, shared by List and Search ───────────────────
   One header for both, so switching between them moves nothing (Paul,
   2026-10-06). Moved here unchanged from quote-builder.html. */
/* ── Header ──────────────────────────────────────────────────────── */
.qb-header {
  background:var(--bg-surface);
  border-block-end:1px solid var(--border);
  padding:.5rem 1rem;
  display:flex; align-items:center; gap:.625rem; flex-wrap:wrap;
  flex-shrink:0;
}
.qb-header-title { font-weight:700; font-size:.9375rem; color:var(--text-primary); flex-shrink:0; }
.qb-header-ref   { font-size:.72rem; color:var(--text-tertiary); font-family:monospace; white-space:nowrap; flex-shrink:0; }
.qb-save-status  { font-size:.72rem; white-space:nowrap; flex-shrink:0; }
.qb-header-sep   { width:1px; height:20px; background:var(--border); flex-shrink:0; }
.qb-header-field { display:flex; align-items:center; gap:.3rem; }
.qb-header-field label { color:var(--text-tertiary); white-space:nowrap; font-size:.72rem; font-weight:500; }
.qb-header-field input {
  border:1px solid var(--border); border-radius:6px;
  padding:.25rem .5rem; font-size:.8rem; width:148px;
  background:var(--bg-surface); color:var(--text-primary); transition:border-color .15s;
}
.qb-header-field input:focus { outline:none; border-color:#c8102e; }
/* Two rows on purpose: the quote's details, then the actions on a line of
   their own. Everything on one line needed ~2,100px, so it wrapped into
   three ragged rows. */
@media (min-width: 769px) {
  /* Wraps rather than running off the edge: at ~1100px and below the
     actions are wider than the screen and PDF Quote was cut off. */
  .qb-header-actions { order:10; width:100%; margin-inline-start:0; flex-wrap:wrap; }
  .qb-header > .admin-link-wrap, .qb-header > #admin-link-wrap { margin-inline-start:auto; }
}
.qb-more { position:relative; }
.qb-more > summary { list-style:none; }
.qb-more > summary::-webkit-details-marker { display:none; }
.qb-more-menu {
  position:absolute; inset-inline-start:0; inset-block-start:calc(100% + .3rem); z-index:30;
  min-width:12rem; padding:.25rem; display:flex; flex-direction:column;
  background:var(--bg-surface); border:1px solid var(--border);
  border-radius:var(--radius-md); box-shadow:var(--shadow-lg, 0 8px 24px rgba(0,0,0,.12));
}
.qb-more-item {
  display:flex; align-items:center; gap:.4rem; background:none; border:none; text-align:start;
  cursor:pointer; padding:.45rem .6rem; border-radius:var(--radius-sm);
  font-size:.82rem; color:var(--text-primary); white-space:nowrap;
}
.qb-more-item:hover { background:var(--bg-surface-2); }
/* Project names run long ("Riverside Hospital Phase 2 — East Wing"); it
   takes most of the room the SSA field used to fill. */
@media (min-width: 769px) { .qb-header-field.wide input { width:280px; } }
.qb-header-actions { display:flex; align-items:center; gap:.5rem; margin-inline-start:auto; flex-shrink:0; }
.qb-currency-group { display:flex; gap:2px; background:var(--bg-surface-2); border-radius:6px; padding:2px; }
.qb-currency-btn {
  padding:.2rem .45rem; border-radius:4px; font-size:.72rem; font-weight:700;
  border:none; cursor:pointer; background:transparent; color:var(--text-tertiary);
  transition:all .15s; letter-spacing:.02em;
}
.qb-currency-btn:hover { color:var(--text-primary); }
.qb-currency-btn.active { background:var(--bg-surface); color:#1a2942; box-shadow:0 1px 3px rgba(0,0,0,.1); }

@media (max-width: 768px) {
  .qb-header-actions { margin-inline-start:0; flex-wrap:wrap; width:100%; }
  .qb-header-field   { flex:1 1 46%; }
  .qb-header-field input { width:100%; min-width:0; }
}
