/* ============================================================
   LIFE SUPPORT SERVICES — Certification Portal · styles
   (rev 2026-07-12b: Title Case — titles, buttons, chips,
    subheads, modal titles, offline chip)
   ============================================================ */

/* RECAPTCHA_BADGE_2026-09-12 — the floating reCAPTCHA tile is fixed to the
   bottom-right corner, so it hovers over whatever scrolls past it; at 375px
   that was the first card under "Is This Your First Time Here?". Moving it
   only relocates the collision — there is no empty corner on a phone.

   Hidden instead, which Google permits while the form carries the disclosure:
   includes/captcha.php renders it under every captcha'd submit button, in
   their wording. If that line is ever removed this rule has to go with it.

   visibility, not display: display:none has historically made some reCAPTCHA
   builds re-run their own layout. This leaves the box where it is and stops
   it painting. */
.grecaptcha-badge { visibility: hidden; }

/* ---------- Login ---------- */
.login { min-height: 100vh; display: grid; grid-template-columns: 1.05fr .95fr; }

/* Sign-in page: one door per kind of code (SIGNIN_CODE_ROUTES_2026-09-12).
   Same visual language as .req-picker__card on request-account.php, which is
   where two of these three doors lead — a person crossing between the two
   pages should not feel they have changed products. Single column always: the
   sign-in column is narrow even on a desktop. */
.login__routes-lead { text-align:center; font-size:13.5px; font-weight:700;
  color:var(--text-muted, #626878); margin:20px 0 10px; }
.login__routes { display:grid; gap:10px; }
.login__route {
  display:block; padding:13px 15px; border-radius:12px;
  border:1px solid var(--border, #d7dbe3); background:#fff;
  text-align:left; text-decoration:none; color:inherit;
  transition:border-color .15s, box-shadow .15s;
}
.login__route:hover,
.login__route:focus {
  border-color: var(--lss-navy-500, #2748a6);
  box-shadow: 0 0 0 3px rgba(39,72,166,.12);
  outline:none;
}
.login__route-title { display:block; font-size:14px; font-weight:700; margin-bottom:3px; }
.login__route-body  { display:block; font-size:12.5px; line-height:1.45;
  color:var(--text-muted, #626878); }
.login__routes-note { font-size:12px; line-height:1.5; color:var(--text-muted, #626878);
  margin:12px 0 0; text-align:center; }

/* FIRST_TIME_DISCLOSURE_2026-09-12 — the three doors fold behind one button.
   <summary> is the button; the UA triangle is removed two ways because Safari
   still wants the -webkit- pseudo-element. display:flex on the summary also
   suppresses the marker in Chrome, so the reset is belt and braces. */
.login__routes-disclosure { margin:20px 0 0; }
/* Explicit, at author specificity. The UA rule that hides a closed
   <details>'s content is overridable by any author `display:`, and
   .login__routes sets display:grid — so without this the cards render under
   the button whether it is open or shut. */
.login__routes-disclosure:not([open]) > .login__routes,
.login__routes-disclosure:not([open]) > .login__routes-note { display:none; }
.login__routes-toggle {
  display:flex; align-items:center; gap:12px; cursor:pointer;
  padding:13px 15px; border-radius:12px;
  border:1px solid var(--lss-navy-500, #2748a6); background:#fff;
  list-style:none; text-align:left;
  transition:border-color .15s, box-shadow .15s, background-color .15s;
}
.login__routes-toggle::-webkit-details-marker { display:none; }
.login__routes-toggle::marker { content:""; }
.login__routes-toggle:hover { background:#f7f9ff; }
.login__routes-toggle:focus-visible {
  box-shadow:0 0 0 3px rgba(39,72,166,.22); outline:none;
}
.login__routes-toggle-text { flex:1 1 auto; min-width:0; }
.login__routes-toggle-title { display:block; font-size:14px; font-weight:700;
  color:var(--lss-navy-600, #1f3a86); margin-bottom:3px; }
.login__routes-toggle-body  { display:block; font-size:12.5px; line-height:1.45;
  color:var(--text-muted, #626878); }
.login__routes-toggle-chev {
  flex:none; width:18px; height:18px; color:var(--lss-navy-500, #2748a6);
  transition:transform .18s ease;
}
.login__routes-toggle-chev svg { width:100%; height:100%; display:block; }
.login__routes-disclosure[open] .login__routes-toggle-chev { transform:rotate(180deg); }
.login__routes-disclosure[open] .login__routes { margin-top:10px; }
@media (prefers-reduced-motion: reduce) {
  .login__routes-toggle,
  .login__routes-toggle-chev { transition:none; }
}

.login__brand { background: var(--lss-gradient-brand-diag); color: #fff; position: relative; overflow: hidden; display: flex; flex-direction: column; justify-content: center; padding: 52px 56px; }
.login__brand::after { content:""; position:absolute; inset:0; background: radial-gradient(80% 60% at 88% 8%, rgba(239,106,46,.30), transparent 55%); }
/* Faint dot-grid texture, fading out toward the bottom (2026-07-03). */
.login__brand::before { content:""; position:absolute; inset:0; pointer-events:none;
  background-image: radial-gradient(rgba(255,255,255,.13) 1px, transparent 1.3px);
  background-size: 26px 26px;
  -webkit-mask-image: linear-gradient(180deg, rgba(0,0,0,.9), rgba(0,0,0,.15) 70%, transparent);
          mask-image: linear-gradient(180deg, rgba(0,0,0,.9), rgba(0,0,0,.15) 70%, transparent); }
/* Blurred drifting glows behind the panel content. */
.login__orb { position:absolute; border-radius:50%; filter:blur(70px); opacity:.45; pointer-events:none; }
.login__orb--a { width:340px; height:340px; background:#ef6a2e; top:-90px; right:-70px; animation: lssOrbDrift 14s ease-in-out infinite alternate; }
.login__orb--b { width:440px; height:440px; background:#4a48a8; bottom:-160px; left:-130px; animation: lssOrbDrift 19s ease-in-out infinite alternate-reverse; }
@keyframes lssOrbDrift { to { transform: translate(34px, 26px) scale(1.08); } }
/* Accent word in the hero — same orange as the Sign in button. */
.login__hero-accent { color: var(--lss-orange-500, #ef6a2e); }
/* EKG trace: draws in, holds, sweeps out — soft orange glow. */
.login__ekg { width:100%; max-width:440px; margin-top:28px; display:block; opacity:.7; }
.login__ekg path { fill:none; stroke: var(--lss-orange-500, #ef6a2e); stroke-width:2.5;
  stroke-linecap:round; stroke-linejoin:round;
  stroke-dasharray: 640; stroke-dashoffset: 640;
  filter: drop-shadow(0 0 7px rgba(239,106,46,.55));
  animation: lssEkgDraw 4.2s linear infinite; }
@keyframes lssEkgDraw { 0% { stroke-dashoffset: 640; } 55% { stroke-dashoffset: 0; }
  78% { stroke-dashoffset: 0; } 100% { stroke-dashoffset: -640; } }
@media (prefers-reduced-motion: reduce) {
  .login__orb { animation: none; }
  .login__ekg path { animation: none; stroke-dashoffset: 0; }
}
.login__brand-top { position: relative; }
.login__brand img.logo { height: 32px; }
.login__brand-mid { position: relative; }
.login__eyebrow { font-weight: 700; font-size: 12px; letter-spacing: .16em; text-transform: uppercase; color: var(--lss-orange-300); margin-bottom: 16px; }
.login__brand h1 { font-family: var(--font-display); font-weight: 800; text-transform: uppercase; font-size: 46px; line-height: 1; letter-spacing: -.01em; color: #fff; margin: 0 0 16px; }
.login__brand p { color: #d6d5ec; font-size: 16px; max-width: 30em; line-height: 1.55; margin: 0; }
/* Selector includes the parent so this beats `.login__brand p` (which
   otherwise wins on specificity and squashed the hero to 16px). */
.login__brand .login__hero { font-family: var(--font-display); font-weight: 800; text-transform: uppercase; font-size: 44px; line-height: 1.05; letter-spacing: -.01em; color: #fff; margin: 0 0 16px; max-width: none; }
.login__brand-bottom { position: relative; display: flex; gap: 28px; }
.login__stat b { font-family: var(--font-display); font-weight: 800; font-size: 28px; color: #fff; display: block; line-height: 1; }
.login__stat span { font-size: 12.5px; color: #b9b8d8; }
.login__form-wrap { display: flex; align-items: center; justify-content: center; padding: 40px; background: var(--surface-card); }
.login__form { width: 100%; max-width: 360px; }
/* SIGNIN_CENTER_2026-09-20 — Andrew: "can we center the sign in and welcome
   message".

   They were the only left-aligned things in that column. The logo above them
   fills the card's width, and everything below is centred or full-bleed — the
   passkey button, the "or use your password" divider, the Remember-me row,
   the Sign In button. Two lines hugging the left edge under a full-width logo
   read as a misalignment rather than a choice.

   The 2FA page shares this card and moves with it, which is the point: the
   two pages are consecutive steps of one sign-in, and a heading that jumps
   left between them would look like a different site. The FIELDS stay left
   aligned in both — a label belongs over the left edge of its input. */
.login__form h1, .login__form h2 { font-size: 28px; margin: 0 0 4px; text-align: center; }
.login__reallogo { height: 96px; width: auto; max-width: 100%; object-fit: contain; margin-bottom: 26px; display: block; --logo-text-size: 34px; }
.login__form .sub { color: var(--text-muted); font-size: 15px; margin: 0 0 26px; text-align: center; }
.login__fields { display: flex; flex-direction: column; gap: 16px; margin-bottom: 18px; }
.login__roles { margin-top: 30px; padding-top: 24px; border-top: 1px solid var(--border-subtle); }
.login__roles-label { font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--text-faint); margin-bottom: 12px; }
.role-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.role-btn { display: flex; align-items: center; gap: 11px; padding: 12px 14px; border: 1px solid var(--border-default); border-radius: var(--radius-md); background: #fff; cursor: pointer; text-align: left; font-family: var(--font-sans); transition: border-color .15s, box-shadow .15s, transform .12s; }
.role-btn:hover { border-color: var(--lss-navy-400); box-shadow: var(--shadow-sm); }
.role-btn:active { transform: translateY(1px); }
.role-btn__ic { width: 34px; height: 34px; border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center; flex: none; color: #fff; }
.role-btn__ic svg { width: 18px; height: 18px; }
.role-btn b { font-size: 13.5px; color: var(--text-strong); display: block; line-height: 1.2; }
.role-btn span { font-size: 11.5px; color: var(--text-muted); }

/* ---------- App shell ---------- */
.papp { display: grid; grid-template-columns: 244px 1fr; min-height: 100vh; background: var(--color-bg); }
.psb { background: var(--lss-navy-900); color: #c7c6e0; display: flex; flex-direction: column; padding: 20px 14px; gap: 3px; position: sticky; top: 0; height: 100vh; }
/* Sidebar logo — applied to either the uploaded <img> or the text-wordmark
   <span> fallback. --logo-text-size is consumed by the inline style in
   logo_html() so the two variants render at a comparable visual weight. */
.psb__logo {
    height: 44px;
    max-width: 200px;
    width: auto;
    object-fit: contain;
    /* Center the wordmark inside the sidebar both horizontally and
       within its own box, with equal padding on all four sides so it
       reads as a balanced, framed brand mark rather than a left-flush
       label. The PNG/SVG content stays at 200×44; padding provides the
       breathing room and align-self centers the box itself. */
    object-position: center center;
    margin: 0 0 14px;
    padding: 14px 12px;
    box-sizing: content-box;
    align-self: center;
    --logo-text-size: 22px;
}
/* When the wordmark text fallback is rendered, the .psb__logo class lands on
   a <span>. Heights only apply to replaced elements (img/svg); for the span
   we set line-height so the brand has equivalent vertical presence. */
span.psb__logo { line-height: 44px; height: auto; }
.psb__loc { display: flex; align-items: center; gap: 9px; margin: 4px 6px 16px; padding: 9px 10px; border-radius: var(--radius-md); background: rgba(255,255,255,.05); }
.psb__loc svg { width: 15px; height: 15px; color: var(--lss-orange-400); flex: none; }
.psb__loc b { color: #fff; font-size: 12.5px; display: block; line-height: 1.2; }
.psb__loc span { font-size: 11px; color: #9694b8; }
/* QR code rendering — includes/certificate.php intentionally rasterizes the
   bitmap at 3x the container width for crisp cert printing. Without these
   rules the rendered <canvas> / <img> overflows its container visibly
   (e.g. the 200x200 slot on account-security.php showed a 600x600 QR).
   Scale the bitmap down to fit the slot while preserving aspect ratio —
   the 3x bitmap is still there underneath for high-DPI output. */
[data-qr] { overflow: hidden; }
[data-qr] img, [data-qr] canvas { display: block; max-width: 100%; max-height: 100%; height: auto; width: auto; margin: 0 auto; }

/* Each nav group is a <details> so the user can collapse sections they
   don't need. Section containing the active page is opened by PHP. */
.psb__group { display: flex; flex-direction: column; gap: 3px; }
.psb__sec { font-size: 10.5px; letter-spacing: .12em; text-transform: uppercase; color: #6f6d99; margin: 16px 12px 6px; font-weight: 700; cursor: pointer; user-select: none; display: flex; align-items: center; list-style: none; }
.psb__sec::-webkit-details-marker { display: none; }
.psb__sec::after { content: ''; margin-left: auto; width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 5px solid currentColor; opacity: .6; transition: transform .15s ease; }
.psb__group:not([open]) > .psb__sec::after { transform: rotate(-90deg); }
.psb__sec:hover { color: #9694b8; }
.psb__sec:focus-visible { outline: 2px solid var(--lss-orange-500); outline-offset: 2px; border-radius: 4px; }
/* Reserve the sidebar's own scrollbar gutter so the nav items don't jump
   ~15px sideways when its overflow scrollbar appears/disappears between
   pages (each page auto-expands a different section, changing nav height). */
/* 2026-07-12: overflow-x hidden — anything a hair wider than the rail
   (badge at the end of a long label, the ⌘K search hint, scrollbar-gutter
   reservation) was triggering a horizontal scrollbar. Labels already
   ellipsize (.psb__label), so clipping is the correct behavior. */
.psb { overflow-y: auto; overflow-x: hidden; scrollbar-gutter: stable; }
.psb__item { display: flex; align-items: center; gap: 11px; padding: 9px 12px; border-radius: var(--radius-md); font-size: 14px; font-weight: 600; color: #c7c6e0; cursor: pointer; border: 0; background: none; font-family: var(--font-sans); width: 100%; text-align: left; transition: background .12s, color .12s; }
.psb__item svg { width: 18px; height: 18px; flex: none; opacity: .85; }
.psb__item > span[aria-hidden="true"] { display: inline-flex; align-items: center; flex: none; }
.psb__label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* HOVER_WASH_FLICKER_FIX_2026-07-24 — Andrew: "active buttons on left
   hand side flicker … the orange button flickers." tokens.css ships a
   generic card-anchor wash (a[class*="__item"]:hover → --hover-surface,
   specificity 0,2,1) that out-ranks both rules below at their natural
   0,2,0 — so hovering ANY sidebar link painted the light-navy wash meant
   for white cards over the dark rail, and on the ACTIVE item the .12s
   background transition visibly faded the orange out and back on every
   hover (the reported flicker). Scoping with .psb lifts these to 0,3,0 /
   0,3,1 so the rail's own colors win again; is-active also pins its
   :hover state so the active page stays solid orange under the cursor. */
.psb .psb__item:hover { background: rgba(255,255,255,.06); color: #fff; }
.psb .psb__item.is-active,
.psb .psb__item.is-active:hover { background: var(--lss-orange-500); color: #fff; }
.psb__item.is-active svg { opacity: 1; }
.psb__badge { margin-left: auto; background: rgba(255,255,255,.16); color: #fff; font-size: 11px; font-weight: 700; padding: 1px 7px; border-radius: var(--radius-pill); }
.psb__item.is-active .psb__badge { background: rgba(0,0,0,.18); }
.psb__spacer { flex: 1; }

/* ---------- Sidebar consolidation (2026-06-27) ---------- */

/* Search trigger button — sits just below the location card. The kbd
   chip mirrors the macOS/Linux platform key, swapped by JS at runtime
   for non-mac (Cmd → Ctrl). */
.psb__search {
  display: flex; align-items: center; gap: 9px;
  margin: 0 6px 6px;
  padding: 9px 10px;
  width: calc(100% - 12px);
  border: 0; border-radius: var(--radius-md);
  background: rgba(255,255,255,.05);
  color: #9694b8;
  font-family: var(--font-sans); font-size: 13.5px;
  cursor: pointer;
  text-align: left;
  transition: background .12s, color .12s;
}
.psb__search:hover { background: rgba(255,255,255,.10); color: #fff; }
.psb__search:focus-visible { outline: 2px solid var(--lss-orange-500); outline-offset: 2px; }
.psb__search svg { width: 15px; height: 15px; opacity: .85; flex: none; }
.psb__search-label { flex: 1; }
.psb__search-kbd {
  font: 600 11px var(--font-sans); padding: 2px 6px;
  background: rgba(255,255,255,.10); color: #c7c6e0;
  border-radius: 5px; flex: none;
  display: inline-flex; align-items: center; gap: 2px;
}
.psb__search-kbd-cmd { font-size: 12px; line-height: 1; }

/* Pinned strip (Frequently used) — same group shell as a regular
   section but with a star pill in the header. Always sits at the top. */
.psb__group--pinned > .psb__sec .psb__pin-pill {
  margin-left: 6px; color: var(--lss-orange-400, #f08a52);
  font-size: 12px; line-height: 1;
}

/* Subsection headers inside a section (2026-07-02 admin nav re-org).
   Non-interactive label rows emitted by page_start() when an item carries
   a 'sub' key. Quieter than .psb__sec — smaller, dimmer, no chevron. */
.psb__subhead {
  font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  color: #5b598a; font-weight: 700; margin: 10px 12px 2px;
  user-select: none;
}

/* Footer-style account divider — separates personal items from the
   operational scroll. */
.psb__divider {
  height: 1px; background: rgba(255,255,255,.10);
  margin: 14px 14px 6px;
}
.psb__group--account > .psb__sec { color: #5b598a; }

.psb__user { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: var(--radius-md); background: rgba(255,255,255,.05); width: 100%; border: 0; cursor: pointer; text-align: left; font-family: var(--font-sans); transition: background var(--dur-fast) var(--ease-standard); }
.psb__user:hover { background: rgba(255,255,255,.12); }
.psb__avatar { width: 34px; height: 34px; border-radius: 50%; background: var(--lss-orange-500); color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 13px; flex: none; }
.psb__user b { color: #fff; font-size: 13px; display: block; line-height: 1.2; }
.psb__user span { font-size: 11.5px; color: #9694b8; }

/* L4 (FOLLOWUPS_ROLLUP_2026-07-17): z-stack cheat sheet — grep for a
   number below before adding a new one. Ordered top-to-bottom (front to back).
     9000  .lss-cookie-banner    consent strip (includes/consent_banner.php)
      211  .cmdk                 Cmd-K palette content (v88 Cmd-K > drawer)
      210  .cmdk-scrim           Cmd-K palette backdrop
       90  .npanel               notification bell dropdown
       80  .pmodal-scrim         modal backdrop (must beat .pdrawer)
       70  .pdrawer              side drawer content (page-level)
       40  .pmega__panel         topbar mega-menu panel (child ctx)
       40  .ra__list / .rcp__list / .walkin__menu
                                 in-page search dropdowns — MUST beat the
                                 topbar, or they tie with it at 30 and the
                                 winner is whichever came later in the DOM
                                 (Z_PICKER_ABOVE_TOPBAR_2026-09-17)
       30  .ptb / .ptbsec        sticky topbar + sticky sub-nav
       25  .thread-reply / .clock-sticky   sticky bottom action bars
       20  .lss-form-actions     PCR/EMS Submit + Save-draft bar
   Bell (90) intentionally beats drawer (70) so it can drop over drawer
   content; modal (80) beats bell so a modal always wins the frontmost lane. */
/* topbar */
.ptb { height: 60px; background: #fff; border-bottom: 1px solid var(--border-subtle); display: flex; align-items: center; gap: 18px; padding: 0 26px; position: sticky; top: 0; z-index: 30; }
.ptb__crumb { display: flex; align-items: center; gap: 9px; font-size: 14px; white-space: nowrap; }
.ptb__crumb-role { font-weight: 700; color: var(--lss-navy-700); letter-spacing: .01em; }
.ptb__crumb-sep { color: var(--lss-ink-300); }
.ptb__crumb-page { color: var(--text-muted); font-weight: 500; }
.ptb__right { margin-left: auto; display: flex; align-items: center; gap: 12px; }
.ptb__role { display: flex; align-items: center; gap: 8px; background: var(--lss-ink-50); border: 1px solid var(--border-default); border-radius: var(--radius-md); padding: 6px 10px 6px 12px; }
.ptb__role label { font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--text-faint); white-space: nowrap; }
.ptb__role select { border: 0; background: none; font-family: var(--font-sans); font-weight: 600; font-size: 13.5px; color: var(--lss-navy-700); cursor: pointer; outline: none; padding-right: 2px; border-radius: 4px; }
/* Restore keyboard focus indicator for the admin role switcher — outline:none
   above kills the native ring, which broke keyboard nav (2026-07-17 UX_UI_AUDIT). */
.ptb__role select:focus-visible { box-shadow: 0 0 0 3px rgba(232,90,50,.45); }
.pmain { min-width: 0; display: flex; flex-direction: column; }
.pcontent { padding: 26px; flex: 1; max-width: 1680px; width: 100%; margin: 0 auto; }

/* page header */
.phead { display: flex; align-items: flex-end; justify-content: space-between; gap: 18px; margin-bottom: 22px; flex-wrap: wrap; }
.phead h1, .phead h2 { font-size: 26px; margin: 0; line-height: 1.2; }
.phead p { color: var(--text-muted); margin: 5px 0 0; font-size: 14px; }
.phead__actions { display: flex; gap: 10px; flex-wrap: wrap; }

/* generic panel */
.ppanel { background: #fff; border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); }
.ppanel__head { padding: 16px 20px; border-bottom: 1px solid var(--border-subtle); display: flex; align-items: center; gap: 12px; }
.ppanel__head h3 { font-size: 17px; margin: 0; }
.ppanel__body { padding: 20px; }

/* KPIs */
/* auto-fit so a 5th tile (e.g. recerts-due, demoted from its own card
   2026-07-04) flows in cleanly; 4 tiles still fill the row as before. */
/* M9 (FOLLOWUPS_ROLLUP_2026-07-17): dropped minmax floor from 170px→140px
   so the 5-tile compliance KPI row (local admin dashboard) fits in one
   row at 614–819px viewports instead of wrapping 4+1 with the odd tile
   alone on its own line. 3-tile admin KPI rows are unaffected — they
   already had headroom. */
.pkpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 14px; margin-bottom: 22px; }
.pkpi { background: #fff; border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); padding: 16px 18px; box-shadow: var(--shadow-xs); position: relative; }
.pkpi__n { font-family: var(--font-display); font-weight: 800; font-size: 30px; color: var(--text-strong); line-height: 1; }
.pkpi__l { font-size: 12.5px; color: var(--text-muted); margin-top: 5px; }
.pkpi__accent { color: var(--lss-orange-600); }
.pkpi__warn { color: var(--lss-warning); }
.pkpi__ic { position: absolute; right: 14px; top: 14px; width: 20px; height: 20px; color: var(--lss-ink-300); }
a.pkpi { display: block; text-decoration: none; color: inherit; cursor: pointer; transition: border-color .12s ease, box-shadow .12s ease, transform .12s ease; }
a.pkpi:hover { border-color: var(--lss-orange-300); box-shadow: var(--shadow-sm); transform: translateY(-1px); }
a.pkpi:focus-visible { outline: 2px solid var(--lss-orange-500); outline-offset: 2px; }
.pkpi.is-active { border-color: var(--lss-orange-500); box-shadow: 0 0 0 1px var(--lss-orange-500); }
.pkpi--mute { cursor: default; }
.pkpi--mute:hover { border-color: var(--border-subtle); box-shadow: var(--shadow-xs); transform: none; }

/* 2026-07-13 public dashboard polish — KPI tile tone variants +
   secondary "sub" line. The tone variants add a coloured left rail so
   the meaning of the tile is legible without reading the label:
   danger (expired), warn (expiring / payment pending), ok (all good).
   .pkpi__sub renders under the label as a small-text context line
   ("Next: BLS-CPR — Sat 5 Aug 2026, 9:00 AM"). */
.pkpi__sub { font-size: 12px; color: var(--text-muted); margin-top: 6px; line-height: 1.35;
             display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.pkpi--danger { border-left: 3px solid var(--lss-emergency); }
.pkpi--danger .pkpi__n { color: var(--lss-emergency); }
.pkpi--warn   { border-left: 3px solid var(--lss-warning); }
.pkpi--warn   .pkpi__n { color: #9a6406; }
.pkpi--ok     { border-left: 3px solid var(--lss-success); }
.pkpi--ok     .pkpi__n { color: var(--lss-success); }

/* "Your next things" action strip on my-account.php. Card list of up to
   5 actionable items, each with a coloured left rail matching urgency,
   an icon, a title + short description, and a CTA button. Stacks
   vertically at all widths (already vertical, so no breakpoint work). */
.next-strip__head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
.next-strip__list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 8px; }
.next-item { display: flex; align-items: flex-start; gap: 12px; padding: 12px 14px;
             background: var(--lss-ink-50, #f5f6fc); border: 1px solid var(--border-subtle);
             border-left-width: 3px; border-radius: var(--radius-md); }
.next-item__ic { flex: 0 0 auto; width: 22px; height: 22px; color: var(--text-muted); display: flex; align-items: center; justify-content: center; margin-top: 1px; }
.next-item__ic svg { width: 20px; height: 20px; }
.next-item__body { flex: 1 1 auto; min-width: 0; }
.next-item__title { font-weight: 600; font-size: 14px; color: var(--text-strong); line-height: 1.3; }
.next-item__desc { font-size: 12.5px; color: var(--text-muted); margin-top: 3px; line-height: 1.4; }
.next-item__cta { flex: 0 0 auto; align-self: center; white-space: nowrap; }
.next-item--danger { border-left-color: var(--lss-emergency); background: var(--lss-emergency-bg, #fdf1f0); }
.next-item--danger .next-item__ic { color: var(--lss-emergency); }
.next-item--warn   { border-left-color: var(--lss-warning);   background: var(--lss-warning-bg, #fff6de); }
.next-item--warn   .next-item__ic { color: #9a6406; }
.next-item--info   { border-left-color: var(--lss-navy-500, #4f56b3); }
.next-item--info   .next-item__ic { color: var(--lss-navy-700); }

/* Certificate list on my-account.php — stacked card rows (mobile-first;
   table version scrolled off phones). Wraps action buttons onto their
   own row on phone; keeps them inline with the row on tablet+. */
.cert-hd { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
.cert-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 10px; }
.cert-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px;
            padding: 12px 14px; background: #fff; border: 1px solid var(--border-subtle);
            border-left-width: 3px; border-radius: var(--radius-md); flex-wrap: wrap; }
.cert-row--expired  { border-left-color: var(--lss-emergency); }
.cert-row--expiring { border-left-color: var(--lss-warning); }
.cert-row--active   { border-left-color: var(--lss-success); }
.cert-row--revoked  { border-left-color: var(--lss-ink-300); background: var(--lss-ink-50, #f5f6fc); }
.cert-row__main    { flex: 1 1 240px; min-width: 0; }
.cert-row__title   { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.cert-row__title strong { font-size: 15px; color: var(--text-strong); }
.cert-row__meta    { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 4px; font-size: 12.5px; color: var(--text-muted); }
.cert-row__ref     { color: var(--text-faint); font-size: 11.5px; }
.cert-row__actions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.cert-days         { font-weight: 600; padding: 1px 6px; border-radius: 4px; }
.cert-days--danger { color: var(--lss-emergency); background: var(--lss-emergency-bg, #fdf1f0); }
.cert-days--warn   { color: #9a6406; background: var(--lss-warning-bg, #fff6de); }
@media (max-width: 560px) {
  .cert-row { flex-direction: column; }
  .cert-row__actions { width: 100%; }
  .cert-row__actions .lss-btn { flex: 1 1 auto; justify-content: center; }
  .pkpis { grid-template-columns: 1fr 1fr; gap: 8px; }
  .pkpi { padding: 12px 14px; }
  .pkpi__n { font-size: 24px; }
  .next-item { flex-wrap: wrap; }
  .next-item__cta { align-self: stretch; width: 100%; text-align: center; }
}

/* ---------- Calendar ---------- */
.cal { background: #fff; border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); overflow: hidden; }
.cal__bar { display: flex; align-items: center; gap: 14px; padding: 14px 18px; border-bottom: 1px solid var(--border-subtle); }
.cal__month { font-family: var(--font-display); font-weight: 700; font-size: 20px; color: var(--text-strong); }
.cal__nav { display: flex; gap: 6px; }
.cal__nav button { width: 32px; height: 32px; border: 1px solid var(--border-default); background: #fff; border-radius: var(--radius-sm); cursor: pointer; color: var(--lss-navy-700); display: flex; align-items: center; justify-content: center; }
.cal__nav button:hover { background: var(--lss-ink-50); }
.cal__nav svg { width: 16px; height: 16px; }
.cal__legend { margin-left: auto; display: flex; gap: 14px; flex-wrap: wrap; }
.cal__legend span { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted); }
.cal__legend i { width: 10px; height: 10px; border-radius: 3px; }
/* CAL_LEGEND_ICON_2026-08-11 — Andrew, from an iPhone: "in the calendar legend
   there is no render of the icon."

   icon() emits an <svg> carrying a viewBox and no width or height, so every
   caller sizes it by stretching it inside a fixed-size inline-flex span. That
   works in Chrome, and the legend markup renders correctly there in isolation
   against this very stylesheet — which is exactly why it needed a different
   explanation. Safari resolves the size of an SVG flex item with no intrinsic
   dimensions differently, and `.cal__legend span` sets align-items:center,
   removing the stretch the glyph was relying on to get its height. The same
   icon inside .cal__event keeps its size because that span sets flex:none in a
   row that stretches its items.

   Rather than depend on stretch at all, size the glyph outright. Costs nothing
   where it already worked. */
.cal__legend svg { width: 12px; height: 12px; flex: none; }

/* ==========================================================================
 * ICON_STRETCH_GUARD_2026-08-12 — the general case of the calendar-legend bug.
 *
 * icon() emits an <svg> with a viewBox and no width or height, so it takes its
 * size from its parent. Where the parent is a CLASS, portal.css already sizes
 * the glyph explicitly — 41 such rules, .psb__item svg, .lss-btn__icon svg,
 * .detail-line__ic svg and the rest — and those are immune.
 *
 * The exposed case is a bare <span style="width:13px;height:13px;display:
 * inline-flex"> with no class, which sizes the glyph purely by stretching it.
 * That works until an ancestor sets align-items:center, at which point the
 * stretch disappears and Safari — unlike Chrome — resolves the dimensionless
 * SVG to nothing. That is exactly how the legend triangle vanished on an
 * iPhone while rendering correctly on every desktop we looked at.
 *
 * Scoped to span:not([class]) on purpose. The classless spans are all small
 * chips, 13-15px, where filling the box IS the intent. The bigger wrappers —
 * 28px circular badges with a background colour, where the glyph is meant to
 * sit inside the circle rather than fill it — all carry classes and keep their
 * own rules untouched.
 * ========================================================================== */
span:not([class])[style*="inline-flex"] > svg { width: 100%; height: 100%; }
.cal__grid { display: grid; grid-template-columns: repeat(7, 1fr); }
.cal__dow { padding: 9px 12px; font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--text-faint); border-bottom: 1px solid var(--border-subtle); text-align: left; }
.cal__cell { min-height: 104px; border-right: 1px solid var(--border-subtle); border-bottom: 1px solid var(--border-subtle); padding: 7px 8px; display: flex; flex-direction: column; gap: 5px; }
.cal__cell:nth-child(7n) { border-right: 0; }
.cal__cell--out { background: var(--lss-ink-50); }
.cal__date { font-size: 12.5px; font-weight: 600; color: var(--text-muted); }
.cal__cell--today .cal__date { background: var(--lss-navy-700); color: #fff; width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.cal__event { display: flex; align-items: center; gap: 6px; padding: 4px 7px; border-radius: var(--radius-xs); font-size: 11.5px; font-weight: 600; color: #fff; cursor: pointer; border: 0; font-family: var(--font-sans); text-align: left; width: 100%; transition: filter .12s; }
.cal__event:hover { filter: brightness(1.08); }
.cal__event .ec { font-family: var(--font-mono); font-weight: 700; }
.cal__event--open { box-shadow: inset 0 0 0 1.5px rgba(255,255,255,.5); }
.cal__event-loc { opacity: .85; margin-left: auto; font-weight: 500; }

/* ---------- Documents ---------- */
.doc-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 18px; flex-wrap: wrap; }
.doc-cats { display: flex; gap: 8px; flex-wrap: wrap; }
.doc-chip { padding: 7px 13px; border-radius: var(--radius-pill); border: 1px solid var(--border-default); background: #fff; font-size: 13px; font-weight: 600; color: var(--lss-ink-600); cursor: pointer; font-family: var(--font-sans); }
.doc-chip:hover { border-color: var(--lss-navy-300); color: var(--lss-navy-700); }
.doc-chip.is-active { background: var(--lss-navy-700); border-color: var(--lss-navy-700); color: #fff; }
.doc-list { display: flex; flex-direction: column; }
/* 2026-07-19 (doc-directory column alignment): head row and data rows were
   each their own grid — the head's last column resolved to ~53px (just the
   "Manage" label) while data rows' last column ballooned to ~182px (dropdown
   + 2 icon buttons). Because col 2 uses 1fr, that desync flexed col 2 to
   different widths per row, and every column downstream drifted right/left.
   Fix: pin the last two columns to fixed widths that fit their WIDEST
   content, so the head row and every data row share the same track sizes
   and the columns line up on every row.
   - Non-admin: Access badge (130px) · Updated date + action buttons (200px)
   - Admin:     Access badge (130px) · Visibility select (140px) ·
                Manage cell = 130px select + 2 × ~34px icons + 2 × 8px gaps
                ≈ 214px, rounded to 220px for breathing room.
   Head cells inherit the same tracks, so "Access", "Visibility", "Manage"
   labels align squarely over their column content. */
.doc-row { display: grid; grid-template-columns: 42px minmax(0, 1fr) 130px 140px 200px; align-items: center; gap: 16px; padding: 14px 18px; border-bottom: 1px solid var(--border-subtle); }
.doc-row > * { min-width: 0; }
.doc-row--admin { grid-template-columns: 42px minmax(0, 1fr) 130px 140px 220px; }
.doc-row--head { padding: 10px 18px; background: var(--lss-ink-50); border-bottom: 1px solid var(--border-subtle); font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--text-faint); font-weight: 700; }
.doc-row--head > div { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mini-select { appearance: none; -webkit-appearance: none; font-family: var(--font-sans); font-size: 12.5px; font-weight: 600; color: var(--lss-navy-700); background: #fff; border: 1px solid var(--border-default); border-radius: var(--radius-sm); padding: 6px 24px 6px 10px; cursor: pointer; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b6e7a' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 7px center;
  /* Keep the dropdown a predictable width regardless of the selected
     option text. Without this the <select> sizes to its current label,
     so a long option like "Instructors & admins" silently widens the cell
     and visually collides with the Visibility text column to the left. */
  max-width: 130px; width: 130px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.mini-select:focus { outline: none; border-color: var(--lss-orange-400); box-shadow: var(--shadow-focus); }
.doc-row:last-child { border-bottom: 0; }
.doc-ic { width: 38px; height: 44px; border-radius: var(--radius-xs); display: flex; align-items: flex-end; justify-content: center; padding-bottom: 5px; font-family: var(--font-display); font-weight: 800; font-size: 9px; color: #fff; letter-spacing: .04em; }
/* LIB_PICKER_2026-09-12 — a default, so an unrecognised type is never
   invisible. The base rule sets `color:#fff` and only these two modifiers ever
   set a background, which meant every other kind of file rendered white on
   nothing: on this install, every VIDEO and every PPTX. A neutral slate is a
   worse badge than a right-coloured one and infinitely better than no badge. */
.doc-ic { background: #475569; }
.doc-ic--pdf  { background: #c0392b; }
.doc-ic--docx, .doc-ic--doc { background: #2b6cb0; }
.doc-ic--pptx, .doc-ic--ppt { background: #c2410c; }
.doc-ic--xlsx, .doc-ic--xls, .doc-ic--csv { background: #15803d; }
.doc-ic--video, .doc-ic--mp4, .doc-ic--mov, .doc-ic--webm { background: #6d28d9; }
.doc-ic--image, .doc-ic--png, .doc-ic--jpg, .doc-ic--jpeg, .doc-ic--gif { background: #0e7490; }
.doc-ic--zip { background: #854d0e; }
.doc-ic--link { background: #334155; }
.doc-name { font-weight: 600; color: var(--text-strong); font-size: 14.5px; }
.doc-meta { font-size: 12.5px; color: var(--text-muted); }
.doc-mono { font-family: var(--font-mono); font-size: 12.5px; color: var(--text-muted); }

/* secure / view-only document viewer */
/* @dead audit-2026-07-17 — entire .doc-viewer* module has zero runtime refs
   (parent .doc-viewer and children __banner/__lines/__page/__sheet/__wm never
   rendered). Kept commented pending 30-day soak.
.doc-viewer__banner { display: flex; align-items: center; gap: 9px; font-size: 13px; font-weight: 600; color: var(--lss-info); background: var(--lss-info-bg); border: 1px solid color-mix(in srgb, var(--lss-info) 25%, #fff); border-radius: var(--radius-md); padding: 11px 14px; margin-bottom: 14px; }
.doc-viewer__banner.is-secure { color: #9a6406; background: var(--lss-warning-bg); border-color: color-mix(in srgb, var(--lss-warning) 35%, #fff); }
.doc-viewer__page { position: relative; background: var(--lss-ink-100); border-radius: var(--radius-md); padding: 26px; max-height: 56vh; overflow: hidden; }
.doc-viewer__sheet { position: relative; background: #fff; border: 1px solid var(--border-subtle); box-shadow: var(--shadow-md); border-radius: 4px; padding: 30px 34px; max-width: 560px; margin: 0 auto; z-index: 1; }
.doc-viewer__lines { display: flex; flex-direction: column; gap: 11px; }
.doc-viewer__lines span { display: block; height: 9px; border-radius: 3px; background: var(--lss-ink-100); }
.doc-viewer__wm { position: absolute; inset: 0; display: flex; flex-wrap: wrap; gap: 26px 40px; align-content: center; justify-content: center; transform: rotate(-24deg) scale(1.4); pointer-events: none; z-index: 2; opacity: .14; }
.doc-viewer__wm span { font-family: var(--font-display); font-weight: 800; font-size: 15px; letter-spacing: .08em; text-transform: uppercase; color: var(--lss-emergency); white-space: nowrap; }
.doc-viewer { user-select: none; }
*/

/* ---------- Tables (staff, rosters) ---------- */
.ptable { width: 100%; }
.ptable__row { display: grid; align-items: center; gap: 16px; padding: 13px 20px; border-bottom: 1px solid var(--border-subtle); }
.ptable__row:last-child { border-bottom: 0; }
.ptable__head { font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--text-faint); background: var(--lss-ink-50); }

/* ---------- .lss-table (real <table> lists) ----------
   LSS_TABLE_2026-09-12. This class had NO rules anywhere in the portal —
   not thin ones, none — while twenty-seven tables across seventeen pages
   wore it, and twenty of those callsites hand-patched style="width:100%"
   to paper over its absence. Two comments in the codebase already record
   running into it (bulk-staff-import.php rebuilt its preview on .ptable
   rather than fix this; department-invoice.php blames "lss-table's generic
   centering", which never existed) and neither generalised.

   The numbers are lifted from .ptable/.ptable__head above on purpose, so
   the div-grid lists and the real tables read as one product.

   No border or radius on the table itself: .ptable carries none either,
   because the .ppanel around it does, and a table drawing its own would
   double-rule inside every panel it sits in.

   Child combinators throughout, so a nested table inside a cell keeps its
   own styling instead of inheriting this one. */
.lss-table { width: 100%; border-collapse: collapse; }
.lss-table > thead > tr > th {
  font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-faint); background: var(--lss-ink-50);
  text-align: left; padding: 10px 14px; white-space: nowrap;
  border-bottom: 1px solid var(--border-subtle);
}
.lss-table > tbody > tr > td,
.lss-table > tbody > tr > th {
  padding: 12px 14px; border-bottom: 1px solid var(--border-subtle);
  vertical-align: middle; color: var(--text-strong);
}
/* A <th> inside the body is a row label, not a column heading — left, bold,
   and none of the uppercase banding above. */
.lss-table > tbody > tr > th { text-align: left; font-weight: 600; }
.lss-table > tbody > tr:last-child > td,
.lss-table > tbody > tr:last-child > th { border-bottom: 0; }
.lss-table > tbody > tr:hover { background: var(--lss-navy-50); }
/* Opt-in, because the one thing a shared table style cannot guess is which
   column is money. Tabular numerals so the digits line up down the column. */
.lss-table > thead > tr > th.num,
.lss-table > tbody > tr > td.num {
  text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums;
}

/* ---------- .lss-table--stack ----------
   PASSKEYS_2026-09-19c. An opt-in modifier: below 720px the table stops being
   a table and becomes one block per row, each cell carrying the column heading
   it lost as a data-lbl attribute.

   Added for the passkeys list on account-security.php, which has five columns
   AND an editable name box with a button in the first one. Measured at 390px
   before this existed: 696px of table in a 345px panel, clipped, with the
   Remove button off the edge and unreachable — the page did not scroll
   sideways, so there was no way to get to it at all.

   Not applied to .lss-table generally. Twenty-seven tables wear that class and
   a silent layout change to all of them is not a mobile fix, it is a surprise.
   Trusted devices and Remembered devices on the same page have the same shape
   and the same problem (118px over at 390px) and can opt in as they are.

   Specificity ties with the .lss-table rules above, so this block must stay
   BELOW them in the file to win. */
/* ---------- .lss-table--tight ----------
   PASSKEYS_2026-09-20. Narrower side padding, for a table with more columns
   than the panel around it comfortably holds.

   Measured on account-security.php: the panel is capped at ~678px at ANY
   viewport width — 1280 and 2560 give the same 678 — so a six-column list
   cannot be widened by making the window bigger. At the default 14px it came
   out 707px and clipped the Remove button by 29px; at 10px it fits with room
   to spare, and 4px a side is not a spacing anyone will miss. Cheaper than
   dropping a column the reader asked for. */
.lss-table--tight > thead > tr > th,
.lss-table--tight > tbody > tr > td { padding-left: 10px; padding-right: 10px; }

/* The cell that holds a row's buttons. Right-aligned in a table, where the
   eye follows the row across to them; left-aligned when stacked, where there
   is no row to follow and a right-aligned button hangs off the block above it
   looking unattached. Kept out of the markup so the media query below can win
   — an inline style could not be overridden. */
.lss-table__actions { text-align: right; }

@media (max-width: 720px) {
  .lss-table--stack > thead { display: none; }
  .lss-table--stack > tbody > tr {
    display: block; padding: 12px 0;
    border-bottom: 1px solid var(--border-subtle);
  }
  .lss-table--stack > tbody > tr:last-child { border-bottom: 0; }
  .lss-table--stack > tbody > tr:hover { background: transparent; }
  .lss-table--stack > tbody > tr > td {
    display: block; border-bottom: 0; padding: 4px 14px; text-align: left;
  }
  /* The heading the cell lost, printed in front of its value so a bare date
     or IP still says what it is. Cells with no label (the actions cell) get
     nothing rather than an empty run-in. */
  .lss-table--stack > tbody > tr > td[data-lbl]::before {
    content: attr(data-lbl) ": ";
    font-size: 11px; font-weight: 700; letter-spacing: .06em;
    text-transform: uppercase; color: var(--text-faint);
  }
  .lss-table--stack > tbody > tr > td.lss-table__actions {
    text-align: left; padding-top: 10px;
  }
}

.person { display: flex; align-items: center; gap: 11px; }
.person__av { width: 36px; height: 36px; border-radius: 50%; background: var(--lss-navy-100); color: var(--lss-navy-700); display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 13px; flex: none; }
.person b { font-size: 14px; color: var(--text-strong); display: block; line-height: 1.25; }
.person span { font-size: 12px; color: var(--text-muted); }
.cert-pills { display: flex; gap: 6px; flex-wrap: wrap; }
/* CERT_COLUMNS_2026-08-01 — Andrew: "make the certifications on the staff list
   line up vertically across staff." The rule above is why they did not: a
   flex-wrap of whatever each person happened to hold, so BLS sat in a different
   place on every row and answering "who is missing ACLS" meant reading all of
   them.

   staff.php renders the same ordered set of codes in the same slots on every
   row, with a placeholder where the person holds none — a column is a
   certification, and reading down it is the answer.

   auto-fill rather than a fixed column count: every row's cell is the same
   width (the parent row grid decides it), so auto-fill resolves to the same
   number of columns on every row and the alignment survives wrapping when
   there are more codes than fit on one line.

   Lives here rather than in staff.php's <style> because that block is inside
   the profile modal, which only renders when ?profile= is set — the first cut
   put it there and the rules never reached the list at all. */
.cert-pills.cert-pills--grid { display: grid; gap: 6px; align-items: center;
  justify-items: start;                       /* pills keep their own width */
  /* CERT_COLUMNS_MINWIDTH_2026-09-09. Andrew: "user qualifications exceed the
     columns." The wrapping promised just above never happened. A grid ITEM's
     automatic minimum is its min-content, and this cell's min-content is every
     code on one line (12 codes x 105px at Life Support Services), so the row's
     1.5fr Certifications track was forced out to 1326px, the 1.3fr Name track
     was crushed to 102px, and the header — same template, but no wide content
     to fight it — stayed at its honest 609/703 and no longer lined up with a
     single body row. The panel overflowed with it (staff.php pushed the whole
     document sideways). min-width:0 is what lets an fr track shrink past its
     content; auto-fill then does what the note above says, resolving to the
     same 6 columns on every row so the vertical alignment still holds. */
  min-width: 0;
  grid-template-columns: repeat(auto-fill, minmax(52px, 1fr)); }  /* count set per page */
.cert-pills.cert-pills--grid .cert-empty { grid-column: 1 / -1; }
.cert-none { display: block; font-size: 12px; line-height: 1;
  padding: 5px 10px; color: var(--text-faint, #9aa0b4); }
/* The card layout stacks one person per card, so there is no column to line up
   with and the placeholders would be pure noise. */
@media (max-width: 900px) {
  /* CERT_EMPTY_WRAP_2026-09-22 — the per-page inline max-width sizes the
     DESKTOP column grid (codes x column width). In a card there is no grid to
     line up with, and that cap squeezed "No certifications yet" into ~60px
     and split it mid-word. !important because the cap is an inline style. */
  .cert-pills.cert-pills--grid { display: flex; flex-wrap: wrap; max-width: none !important; }
  .cert-none { display: none; }
  .cert-empty { white-space: normal; word-break: normal; overflow-wrap: normal; }
  /* CERT_EMPTY_DUP_2026-09-22 — on a card the standing line right below says
     "No certifications" already; the empty cell only repeated it. */
  .cert-pills.cert-pills--none { display: none; }
}
.standing { display: inline-flex; align-items: center; gap: 7px; font-size: 13px; font-weight: 600; }
.standing__dot { width: 9px; height: 9px; border-radius: 50%; }

/* ---------- Instructor (Connecteam-style shift cards) ---------- */
.shift-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
.shift { background: #fff; border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); overflow: hidden; display: flex; flex-direction: column; }
.shift__top { padding: 16px 18px; display: flex; align-items: flex-start; gap: 12px; border-left: 4px solid var(--lss-orange-500); }
.shift__cal { width: 50px; flex: none; border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); overflow: hidden; text-align: center; }
.shift__cal .m { background: var(--lss-navy-700); color: #fff; font-size: 10px; font-weight: 700; letter-spacing: .08em; padding: 2px 0; text-transform: uppercase; }
.shift__cal .d { font-family: var(--font-display); font-weight: 800; font-size: 22px; color: var(--text-strong); padding: 3px 0; }
/* YEAR_WHEN_IT_DIFFERS_2026-10-01 — only rendered when the date is not in the
   current year, so the chip is unchanged for the dates it normally shows. A
   course a year out read SEP 28 exactly like one two days gone, which is how a
   correct Enroll button came to look like a bug. */
.shift__cal .y { background: var(--lss-gray-100); color: var(--text-muted); font-size: 9.5px;
                 font-weight: 700; letter-spacing: .06em; padding: 1px 0 2px;
                 border-top: 1px solid var(--border-subtle); }
/* ---------- Needs-an-instructor row (SHIFTROW_MOBILE_2026-09-19) ----------
   Was a pile of inline styles on dashboard.php. Moved here so the chip's width
   is not in a style attribute — the max-width:520px net at the bottom of this
   file matches [style*="flex:0 0"] by substring and was stretching the 44px
   date chip to the full width of the panel.

   Desktop values are the previous inline ones, unchanged. */
.shiftrow { display: flex; gap: 12px; align-items: center; padding: 10px 0;
            border-bottom: 1px solid var(--lss-gray-100); min-height: 44px;
            flex-wrap: wrap; }
.shiftrow__cal { width: 44px; }
.shiftrow__info { flex: 1 1 180px; min-width: 0; }
.shiftrow__title { color: var(--text-strong); font-size: 14px; }
.shiftrow__meta { display: flex; gap: 6px; margin-top: 4px; align-items: center; flex-wrap: wrap; }
/* The count and the button are ONE item. Previously the count carried
   margin-left:auto and the button trailed it as a separate flex child, so a
   long title pushed only the button to the next line and a short one did not —
   rows in the same list wrapped differently from each other. */
.shiftrow__tail { margin-left: auto; display: flex; align-items: center; gap: 12px; flex: 0 0 auto; }
.shiftrow__need { font-size: 13px; color: var(--text-strong); }
.shiftrow__act { min-height: 36px; }

@media (max-width: 560px) {
  /* Phone: the title takes what is left of line one, and the count + button
     take a full line under it — count left, button right, every row the same. */
  /* Top-aligned once a title can run to three lines — centred leaves the date
     chip floating in the middle of the block it labels. */
  .shiftrow { align-items: flex-start; }
  .shiftrow__tail { margin-left: 0; flex: 1 1 100%; align-items: center;
                    justify-content: space-between; }
  /* `flex: 1 1 0`, not `auto`. With an auto basis the flex base size is the
     title's max-content width, so a long one — "Semi-Automatic External
     Defibrillation (S-AED) with Medical Direction" — overflows the line and
     wraps the whole block under the date chip, leaving the chip stranded on a
     line of its own. A zero basis makes it take the space that is left and
     wrap its text instead. */
  .shiftrow__info { flex: 1 1 0; min-width: 0; }
}

.shift__info h3 { font-size: 17px; margin: 0 0 3px; }
.shift__info .meta { font-size: 13px; color: var(--text-muted); display: flex; flex-direction: column; gap: 2px; margin-top: 4px; }
.shift__info .meta span { display: flex; align-items: center; gap: 6px; }
.shift__info .meta svg { width: 13px; height: 13px; flex: none; color: var(--lss-ink-400); }
.shift__foot { margin-top: auto; padding: 13px 18px; border-top: 1px solid var(--border-subtle); display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* flex:0 0 auto — the label can never be squeezed by the action buttons into
   letter-per-line wrapping on phones; with flex-wrap above, the buttons drop
   to their own row instead. */
.shift__foot .who { font-size: 12.5px; color: var(--text-muted); display: flex; align-items: center; gap: 7px; flex: 0 0 auto; }
/* SHIFT_FOOT_ICON_2026-09-10. Andrew: "the bell icon looks a little
   aggressive can we fix the sizing."

   .who is a flex row and its icon() SVG carries no width/height of its own, so
   with no rule here it was laid out at the replaced element's default size —
   an ~90px bell that dwarfed the card and squeezed the label into "Open shift —
   / needs an / instructor" over three lines. The identical icons two lines up
   (.meta svg) were fine only because THEY have this rule; the footer never got
   one. Same treatment, same ink, so the bell/clipboard sits at the weight of
   the clock, pin and people icons directly above it. */
.shift__foot .who svg { width: 14px; height: 14px; flex: none; color: var(--lss-ink-400); }
@media (max-width: 640px) {
  .shift__foot .who { flex-basis: 100%; }
}

/* roster (pass/fail) */
.roster-row { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 16px; padding: 13px 20px; border-bottom: 1px solid var(--border-subtle); }
.roster-row:last-child { border-bottom: 0; }
.result-toggle { display: inline-flex; border: 1px solid var(--border-default); border-radius: var(--radius-md); overflow: hidden; }
.result-toggle button { padding: 7px 16px; border: 0; background: #fff; cursor: pointer; font-family: var(--font-sans); font-weight: 600; font-size: 13px; color: var(--text-muted); display: flex; align-items: center; gap: 6px; }
.result-toggle button svg { width: 14px; height: 14px; }
.result-toggle button + button { border-left: 1px solid var(--border-default); }
.result-toggle button.pass.is-on { background: var(--lss-success); color: #fff; }
.result-toggle button.fail.is-on { background: var(--lss-emergency); color: #fff; }

/* attendance */
.att-sessions { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 18px; }
.att-session { position: relative; display: flex; flex-direction: column; gap: 2px; padding: 10px 18px; border: 1px solid var(--border-default); border-radius: var(--radius-md); background: #fff; cursor: pointer; font-family: var(--font-sans); min-width: 96px; }
.att-session:hover { border-color: var(--lss-navy-300); }
.att-session.is-active { border-color: var(--lss-navy-700); background: var(--lss-navy-50); }
.att-session__day { font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--text-faint); }
.att-session__date { font-size: 14px; font-weight: 600; color: var(--text-strong); }
.att-session.is-done .att-session__day { color: var(--lss-success); }
.att-session__check { position: absolute; top: 8px; right: 8px; width: 14px; height: 14px; color: var(--lss-success); }
.att-toggle { display: inline-flex; border: 1px solid var(--border-default); border-radius: var(--radius-md); overflow: hidden; }
.att-toggle button { padding: 7px 14px; border: 0; background: #fff; cursor: pointer; font-family: var(--font-sans); font-weight: 600; font-size: 13px; color: var(--text-muted); display: flex; align-items: center; gap: 6px; }
.att-toggle button svg { width: 14px; height: 14px; }
.att-toggle button + button { border-left: 1px solid var(--border-default); }
.att-toggle button.present.is-on { background: var(--lss-success); color: #fff; }
.att-toggle button.late.is-on { background: var(--lss-warning); color: #3a2600; }
.att-toggle button.absent.is-on { background: var(--lss-emergency); color: #fff; }
.att-allbtn { background: none; border: 0; color: var(--lss-navy-600); font-weight: 600; font-size: 12.5px; cursor: pointer; font-family: var(--font-sans); }
/* 2026-07-18 (LINK_HOVER_STYLE_2026-07-17.md): site-wide preference —
   button-styled links (link that acts as a button) hover on background +
   color, never text-decoration. Neutral wash tint is --hover-surface. */
.att-allbtn:hover { background-color: var(--hover-surface); border-radius: 4px; color: var(--lss-navy-800, #191742); }

/* ---------- Forms (create course / template builder) ---------- */
.form-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.form-2col .full { grid-column: 1 / -1; }
/* GRID_MINWIDTH_2026-07-27: a grid item's automatic minimum size is its
   MIN-CONTENT width, not zero — so one child that can't shrink (a <select>
   whose options read "ACLS — Advanced Cardiac Life Support", a long URL, a
   nowrap hint) pushes its whole track wider than the container and takes the
   grid with it. Measured in the Course types edit modal at a 375px viewport:
   .form-2col itself was 301px while a `.full` child inside it was **1501px**,
   which is why the dialog scrolled sideways and read as "really wide" on a
   phone. `min-width: 0` restores the shrink-to-fit that everyone assumes grid
   already does. Applied to the children, not the container, so the two-column
   track sizing is untouched on desktop. */
.form-2col > * { min-width: 0; }
/* Belt and braces: form controls never exceed their cell. A <select> with long
   option text is the usual offender and the one measured here. */
.form-2col input, .form-2col select, .form-2col textarea { max-width: 100%; }
/* Mobile: collapse to single column so the two fields don't squeeze
   into ~140 px each (the cost / valid-for fields on course-edit
   were overlapping before this rule). */
@media (max-width: 600px) {
  .form-2col { grid-template-columns: 1fr; }
}
.field-label { font-size: 13px; font-weight: 600; color: var(--text-strong); margin-bottom: 6px; display: block; }
.builder { display: grid; grid-template-columns: 340px 1fr; gap: 22px; align-items: start; }
.builder--form { grid-template-columns: 1fr 380px; }
/* Mobile: any .builder layout (designer, form builder, broadcast, report
   builder) becomes single column. Inline `grid-template-columns: …` on
   .builder--form is overridden by the more-specific media-query rule. */
@media (max-width: 820px) {
  .builder, .builder--form { grid-template-columns: 1fr !important; }
}
.builder__controls { display: flex; flex-direction: column; gap: 16px; }
.swatch-row { display: flex; gap: 8px; }
.swatch-btn { width: 30px; height: 30px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 1px var(--border-default); cursor: pointer; }
.swatch-btn.is-on { box-shadow: 0 0 0 2px var(--lss-navy-700); }
.builder__preview { position: sticky; top: 88px; }
.preview-label { font-size: 11px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--text-faint); margin-bottom: 10px; }

/* ---------- Certificate ---------- */
/* container queries: text scales to the certificate's own width at any render size */
.cert { background: #fff; border: 1px solid var(--border-subtle); box-shadow: var(--shadow-md); position: relative; overflow: hidden; container-type: inline-size; }
.cert--letter { aspect-ratio: 1.294 / 1; }              /* US Letter landscape (11 x 8.5) */
.cert--card { aspect-ratio: 1.588 / 1; max-width: 460px; margin: 0 auto; border-radius: 10px; } /* wallet card (CR80) */

/* ---- Full (Letter) layout ---- */
.cert--letter { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 7% 9%; }
.cert__frame { position: absolute; inset: 3%; border: 2px solid var(--cert-accent, #ef6a2e); pointer-events: none; }
.cert__frame::before { content:""; position: absolute; inset: 5px; border: 1px solid rgba(35,33,96,.25); }
.cert__bar { position: absolute; top: 0; left: 0; right: 0; height: 1.4%; background: var(--cert-accent, #ef6a2e); }
.cert__star { width: 8%; max-width: 54px; margin-bottom: 2.4%; }
.cert__org { font-family: var(--font-display); font-weight: 800; text-transform: uppercase; letter-spacing: .12em; font-size: clamp(10px, 1.9cqw, 16px); color: var(--lss-navy-700); }
.cert__eyebrow { font-weight: 700; letter-spacing: .2em; text-transform: uppercase; font-size: clamp(8px, 1.25cqw, 11px); color: var(--cert-accent, #ef6a2e); margin: 3% 0 2.4%; }
.cert__name { font-family: var(--font-display); font-weight: 800; font-size: clamp(20px, 5.6cqw, 46px); color: var(--text-strong); line-height: 1; letter-spacing: -.01em; }
.cert__rule { width: 46%; height: 2px; background: var(--cert-accent, #ef6a2e); margin: 2.6% 0; opacity: .85; }
.cert__sub { font-size: clamp(9px, 1.6cqw, 14px); color: var(--text-muted); max-width: 80%; }
.cert__course { font-family: var(--font-display); font-weight: 700; font-size: clamp(13px, 2.9cqw, 24px); color: var(--lss-navy-700); margin-top: 1.4%; text-transform: uppercase; }
.cert__foot { position: absolute; left: 9%; right: 9%; bottom: 11%; display: flex; align-items: flex-end; justify-content: space-between; }
.cert__sign { text-align: center; }
.cert__sign .sig { font-family: var(--font-sans), cursive; font-style: italic; font-weight: 500; font-size: clamp(12px, 2.3cqw, 19px); color: var(--text-strong); border-bottom: 1px solid var(--border-strong); padding: 0 14px 2px; }
.cert__sign .who { font-size: clamp(7px, 1.15cqw, 10px); color: var(--text-muted); letter-spacing: .04em; text-transform: uppercase; margin-top: 4px; font-weight: 600; }
.cert__seal { width: 13%; max-width: 78px; aspect-ratio: 1; border-radius: 50%; border: 2px solid var(--cert-accent, #ef6a2e); display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--cert-accent, #ef6a2e); }
.cert__seal img { width: 42%; }
.cert__seal span { font-size: clamp(5px, .9cqw, 7px); font-weight: 800; letter-spacing: .1em; text-transform: uppercase; margin-top: 2px; }
.cert__meta { position: absolute; left: 9%; right: 9%; bottom: 4.5%; font-family: var(--font-mono); font-size: clamp(6px, 1.1cqw, 9px); color: var(--text-faint); text-align: center; line-height: 1.5; }

/* ---- Wallet card layout ---- */
.cert--card { display: grid; grid-template-rows: auto 1fr auto; padding: 6.5% 7% 6.5% 8%; text-align: left; }
.cert--card .cert__stripe { position: absolute; left: 0; top: 0; bottom: 0; width: 2.4%; background: var(--cert-accent, #ef6a2e); }
.cert-card__head { display: flex; align-items: center; gap: 2.4%; }
.cert-card__star { width: 7.5%; flex: none; }
.cert-card__org { font-family: var(--font-display); font-weight: 800; text-transform: uppercase; letter-spacing: .08em; font-size: clamp(11px, 3.5cqw, 18px); color: var(--lss-navy-700); line-height: 1; }
.cert-card__tag { margin-left: auto; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; font-size: clamp(6px, 1.9cqw, 9px); color: var(--cert-accent, #ef6a2e); }
.cert-card__body { align-self: center; }
.cert-card__course { font-family: var(--font-display); font-weight: 800; text-transform: uppercase; font-size: clamp(15px, 5.6cqw, 28px); color: var(--lss-navy-700); line-height: .98; letter-spacing: -.01em; }
.cert-card__name { font-family: var(--font-display); font-weight: 700; font-size: clamp(12px, 4.4cqw, 22px); color: var(--text-strong); line-height: 1.05; margin-top: 2%; }
.cert-card__foot { display: flex; align-items: flex-end; gap: 5%; }
.cert-card__field span { display: block; font-size: clamp(6px, 1.9cqw, 9px); font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--text-faint); margin-bottom: 2px; }
.cert-card__field b { font-family: var(--font-mono); font-weight: 600; font-size: clamp(8px, 2.5cqw, 12px); color: var(--text-strong); }
.cert-card__no { margin-left: auto; font-family: var(--font-mono); font-size: clamp(7px, 2.1cqw, 10px); color: var(--text-faint); text-align: right; }
.cert-card__seal { position: absolute; right: 7%; top: 7%; width: 13%; aspect-ratio: 1; border-radius: 50%; border: 1.5px solid var(--cert-accent, #ef6a2e); display: flex; align-items: center; justify-content: center; }
.cert-card__seal img { width: 52%; }

/* Segmented control. Two flavours share the same shell:
   - <.seg> with <button> children (course-builder size toggle, etc.)
   - <.seg> with <label> children wrapping a hidden radio + <span.seg-opt>
     (sector / calendar-visibility / audience pickers). The label flavour
     lets the whole segment be a click target for the radio, so keyboard
     and screen-reader users get proper labelling. */
.seg { display: inline-flex; border: 1px solid var(--border-default); border-radius: var(--radius-md); overflow: hidden; background: #fff; }
.seg button { padding: 8px 16px; border: 0; background: #fff; cursor: pointer; font-family: var(--font-sans); font-weight: 600; font-size: 13px; color: var(--text-muted); white-space: nowrap; }
.seg button + button { border-left: 1px solid var(--border-default); }
.seg button.is-on { background: var(--lss-navy-700); color: #fff; }
.seg label { position: relative; display: inline-flex; cursor: pointer; }
.seg label .seg-opt { display: inline-flex; align-items: center; padding: 8px 16px; font-family: var(--font-sans); font-weight: 600; font-size: 13px; color: var(--text-muted); background: #fff; white-space: nowrap; user-select: none; }
.seg label + label .seg-opt { border-left: 1px solid var(--border-default); }
.seg label .seg-opt.is-on { background: var(--lss-navy-700); color: #fff; }
/* SEG_CHECKED_CSS_2026-09-12 — the selected look now follows the RADIO, not a
   class some script has to remember to move.

   It was class-only, rendered by PHP and updated by per-page JS, and each page
   wired its own. course-edit's handler had been upgraded to also rewrite the
   inline background/color the markup sets; course-create's still only toggled
   the class — and an inline style beats a class rule, so on the Create a course
   page clicking "Individual attendees" checked the radio and moved the class
   while the highlight stayed exactly where it was. The control looked dead, and
   was worse than dead: it saved the option it was not showing.

   :checked is the browser's own answer to this question and needs no script, so
   a page that forgets to wire itself still works. The class rule above stays for
   the handlers that already set it. */
.seg label input[type="radio"]:checked + .seg-opt { background: var(--lss-navy-700); color: #fff; }
.seg label:hover .seg-opt:not(.is-on) { color: var(--text-strong); background: var(--lss-ink-050, #f7f7f8); }
.seg label input[type="radio"]:focus-visible + .seg-opt { outline: 2px solid var(--lss-navy-300); outline-offset: -2px; }

/* Mobile: segmented controls with 3+ options (Cost visibility, Audience,
   Calendar visibility on course-edit) overflow their 390 px row.
   Allow the labels to wrap onto a second line and let each segment
   shrink so the row stays inside the viewport. */
@media (max-width: 480px) {
  .seg { display: flex; flex-wrap: wrap; }
  .seg label, .seg button { flex: 1 1 calc(50% - 1px); min-width: 0; }
  .seg label .seg-opt, .seg button {
    white-space: normal;
    text-align: center;
    padding: 9px 8px;
    font-size: 12.5px;
    line-height: 1.2;
    width: 100%;
    justify-content: center;
  }
  /* Divider between wrapped rows — keep the segmented look. */
  .seg label + label .seg-opt { border-left-width: 0; }
  .seg label .seg-opt { border-left: 1px solid var(--border-default); }
  .seg label:first-child .seg-opt { border-left: 0; }
}
.qual-chip { padding: 6px 11px; border-radius: var(--radius-pill); border: 1px solid var(--border-default); background: #fff; font-family: var(--font-mono); font-weight: 600; font-size: 12px; color: var(--text-muted); cursor: pointer; white-space: nowrap; }
.qual-chip:hover { border-color: var(--lss-navy-300); color: var(--lss-navy-700); }
.qual-chip.is-on { background: var(--lss-success-bg); border-color: var(--lss-success); color: var(--lss-success); }
.icon-btn { width: 32px; height: 32px; border: 0; background: transparent; border-radius: var(--radius-sm); cursor: pointer; color: var(--text-faint); display: inline-flex; align-items: center; justify-content: center; }
.icon-btn:hover { background: var(--lss-ink-100); color: var(--text-body); }
.icon-btn svg { width: 16px; height: 16px; }
.icon-btn--danger:hover { background: var(--lss-emergency-bg); color: var(--lss-emergency); }
/* INVOICE_EXEMPT_2026-09-13: an icon-btn that is currently ON. The exempt
   control is a toggle, and a toggle whose two states look identical is a
   button that tells you nothing — the title says which way the click goes,
   but only after you hover it. */
.icon-btn--on { color: var(--lss-success); background: var(--lss-success-bg); }
.icon-btn--on:hover { background: var(--lss-success-bg); color: var(--lss-success); }
.profile-sec-label { font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--text-faint); margin: 4px 0 8px; }
.crt__label { font-size: 10.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--text-faint); margin-bottom: 2px; }
.lss-textarea { width: 100%; border: 1px solid var(--border-default); border-radius: var(--radius-md); padding: 11px 12px; font-family: var(--font-sans); font-size: 14px; color: var(--text-strong); line-height: 1.6; resize: vertical; }
.lss-textarea:focus { outline: none; border-color: var(--lss-orange-400); box-shadow: var(--shadow-focus); }
.tmpl-var { font-family: var(--font-mono); font-size: 12px; background: var(--lss-navy-50); color: var(--lss-navy-700); padding: 3px 7px; border-radius: var(--radius-xs); }
.hours-input { width: 64px; border: 1px solid var(--border-default); border-radius: var(--radius-sm); padding: 5px 8px; font-family: var(--font-mono); font-weight: 600; font-size: 13px; color: var(--text-strong); }
.hours-input:focus { outline: none; border-color: var(--lss-orange-400); box-shadow: var(--shadow-focus); }
.clock-input { width: 74px; border: 1px solid var(--border-default); border-radius: var(--radius-sm); padding: 5px 6px; font-family: var(--font-mono); font-size: 12.5px; color: var(--text-strong); }
.clock-input:focus { outline: none; border-color: var(--lss-orange-400); box-shadow: var(--shadow-focus); }
.login__browse { display: block; width: 100%; margin-top: 14px; background: none; border: 0; color: var(--lss-navy-600); font-family: var(--font-sans); font-weight: 600; font-size: 14px; cursor: pointer; padding: 6px; }
.login__browse:hover { background-color: var(--hover-surface); border-radius: 6px; color: var(--lss-orange-600); }

/* public catalog */
/* @dead audit-2026-07-17 — entire .pubcat_star_ / .pubcard_star_ module has zero
   runtime refs (public catalog was refactored away from this markup). Kept
   commented pending 30-day soak.
.pubcat { background: var(--color-bg); }
.pubcat__hero { background: var(--lss-gradient-brand-diag); color: #fff; border-radius: var(--radius-lg); padding: 32px 34px; margin-bottom: 22px; position: relative; overflow: hidden; }
.pubcat__hero::after { content:""; position:absolute; right:-30px; top:-30px; width:200px; height:200px; background: radial-gradient(circle, rgba(239,106,46,.4), transparent 65%); }
.pubcat__logo { height: 28px; margin-bottom: 16px; position: relative; }
.pubcat__eyebrow { font-weight: 700; font-size: 12px; letter-spacing: .14em; text-transform: uppercase; color: var(--lss-orange-300); position: relative; }
.pubcat__hero h2 { color: #fff; font-size: 30px; margin: 6px 0 8px; position: relative; }
.pubcat__hero p { color: #d6d5ec; margin: 0; max-width: 44em; position: relative; }
.pubcat__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.pubcard { background: #fff; border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); overflow: hidden; display: flex; flex-direction: column; }
.pubcard__top { height: 92px; position: relative; display: flex; align-items: center; justify-content: center; }
.pubcard__body { padding: 16px 18px; flex: 1; }
.pubcard__body h3 { font-size: 17px; margin: 0 0 10px; }
.pubcard__meta { display: flex; flex-direction: column; gap: 6px; }
.pubcard__meta span { display: flex; align-items: center; gap: 7px; font-size: 13px; color: var(--text-muted); }
.pubcard__meta svg { width: 14px; height: 14px; color: var(--lss-navy-500); }
.pubcard__foot { padding: 13px 18px; border-top: 1px solid var(--border-subtle); display: flex; align-items: center; justify-content: space-between; }
.pubcard__price { font-family: var(--font-display); font-weight: 800; font-size: 22px; color: var(--text-strong); }
@media (max-width: 1080px) { .pubcat__grid { grid-template-columns: 1fr 1fr; } }
*/
.aud-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.aud-card { text-align: left; background: #fff; border: 1px solid var(--border-default); border-radius: var(--radius-md); padding: 12px 14px; cursor: pointer; font-family: var(--font-sans); display: flex; flex-direction: column; gap: 2px; }
.aud-card:hover { border-color: var(--lss-navy-300); }
.aud-card.is-on { border-color: var(--lss-navy-700); background: var(--lss-navy-50); box-shadow: inset 0 0 0 1px var(--lss-navy-700); }
.aud-card b { font-size: 14px; color: var(--text-strong); }
.aud-card span { font-size: 12px; color: var(--text-muted); }
.aud-card em { font-style: normal; font-size: 11.5px; font-weight: 700; color: var(--lss-navy-600); margin-top: 3px; }
.csv-drop { display: flex; align-items: center; gap: 14px; padding: 18px 20px; border: 2px dashed var(--border-default); border-radius: var(--radius-md); cursor: pointer; background: var(--lss-ink-50); transition: border-color .15s, background .15s; }
.csv-drop:hover { border-color: var(--lss-navy-400); background: var(--lss-navy-50); }
.csv-drop__ic { width: 42px; height: 42px; border-radius: 50%; background: var(--lss-navy-100); color: var(--lss-navy-700); display: flex; align-items: center; justify-content: center; flex: none; }
.csv-drop__ic svg { width: 20px; height: 20px; }
.csv-drop b { display: block; font-size: 14px; color: var(--text-strong); }
.csv-drop span span { font-size: 12.5px; color: var(--text-muted); }
.link-btn { display: block; background: none; border: 0; padding: 2px 6px; margin-top: 0; margin-left: -6px; border-radius: 4px; color: var(--lss-navy-600); font-family: var(--font-sans); font-size: 12.5px; font-weight: 600; cursor: pointer; text-align: left; transition: color .12s, background-color .12s; }
.link-btn:hover { color: var(--lss-orange-600); background-color: var(--hover-surface); }
/* LIB_PICKER_2026-09-12 — the document-library picker, shared by
   course-create.php and course-edit.php so there is one picker rather than two
   that drift. Everything here was inline styles duplicated across both pages.

   The category row is the reason this exists: twelve categories wrapped onto
   three lines and pushed the list down to four visible documents in a 360px
   box. One scrollable line gives the list the height back, and the chips are
   still all reachable. */
.lib-pick__cats { display: flex; gap: 6px; flex-wrap: nowrap; overflow-x: auto; overscroll-behavior-x: contain;
                  margin-bottom: 10px; padding-bottom: 4px; scrollbar-width: thin; }
.lib-pick__cats .filter-chip { flex: none; white-space: nowrap; }
.lib-pick__list { max-height: 46vh; min-height: 220px; overflow-y: auto;
                  border: 1px solid var(--border-subtle); border-radius: var(--radius-md); }
.lib-row { display: flex; align-items: center; gap: 12px; padding: 10px 14px; cursor: pointer;
           border-bottom: 1px solid var(--border-subtle); min-height: 52px; }
.lib-row:last-child { border-bottom: 0; }
.lib-row:hover { background: var(--lss-navy-50); }
.lib-row__cb { flex: none; margin: 0; width: 16px; height: 16px; accent-color: var(--lss-navy-700); }
.lib-row .doc-ic { flex: none; width: 34px; height: 34px; font-size: 8.5px; letter-spacing: .02em;
                   padding-bottom: 0; align-items: center; overflow: hidden; }
.lib-row__txt { flex: 1; min-width: 0; }
.lib-row__name { display: block; font-size: 13.5px; font-weight: 700; color: var(--text-strong);
                 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lib-row__meta { display: block; font-size: 12px; color: var(--text-muted); margin-top: 1px;
                 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Ticked rows are tinted so a selection made before scrolling is still obvious
   after it — the checkbox alone is easy to lose in a long list. */
.lib-row.is-picked { background: var(--lss-navy-50); box-shadow: inset 3px 0 0 var(--lss-navy-700); }
.lib-pick__empty { padding: 28px 16px; text-align: center; font-size: 13px; color: var(--text-faint); }

.filter-chip { display: inline-flex; align-items: center; gap: 5px; padding: 7px 12px; border-radius: var(--radius-pill); border: 1px solid var(--lss-navy-700); background: var(--lss-navy-50); color: var(--lss-navy-700); font-family: var(--font-sans); font-weight: 700; font-size: 12.5px; cursor: pointer; }
.filter-chip:hover { background: var(--lss-navy-100); }

/* LSS_CHIP_2026-09-06. Andrew: "can we make the show elements ... pills so
   they are consistent with the rest of the site."

   .lss-chip was used on r7-rosters.php and course-types.php and had NO rule
   anywhere in the site CSS — only a print override — so both pages rendered
   their filter rows as bare underlined links while every other filter row is
   a pill. The class was invented twice and styled never.

   The shape is lifted from the de-facto pill this app already uses: the
   dashboard's "Sort by" row, which hand-rolls exactly this in inline styles
   (see $orgSortLink in dashboard.php). Naming it here means the next filter
   row can use the class instead of copying the inline block again. */
.lss-chip { display: inline-flex; align-items: center; gap: 6px; padding: 6px 13px;
  border-radius: var(--radius-pill, 999px); border: 1px solid var(--border-default);
  background: var(--surface-card, #fff); color: var(--text-body);
  font-family: var(--font-sans); font-size: 12.5px; font-weight: 600;
  text-decoration: none; white-space: nowrap; min-height: 34px;
  /* border-box, or min-height lands on the CONTENT box and the padding stacks
     on top of it — 44 + 18 + 2 gave 64px pills on a phone. */
  box-sizing: border-box; touch-action: manipulation; }
.lss-chip:hover { border-color: var(--lss-navy-300, #b9bde0); color: var(--text-strong); }
.lss-chip:focus-visible { outline: 2px solid var(--lss-navy-300, #b9bde0); outline-offset: 2px; }
.lss-chip--on { border-color: var(--lss-navy-700); background: var(--lss-navy-700); color: #fff; }
.lss-chip--on:hover { border-color: var(--lss-navy-700); color: #fff; }
/* Same 44px touch floor the rest of the mobile work settled on. */
@media (max-width: 820px) { .lss-chip { min-height: 44px; padding: 9px 15px; } }

/* ---------- Modal (cert preview / download) ---------- */
.pmodal-scrim { position: fixed; inset: 0; background: rgba(16,14,46,.55); z-index: 80; display: flex; align-items: center; justify-content: center; padding: 32px; opacity: 0; pointer-events: none; transition: opacity .2s; }
.pmodal-scrim.is-open { opacity: 1; pointer-events: auto; }
.pmodal { background: var(--color-bg); border-radius: var(--radius-xl); box-shadow: var(--shadow-xl); width: 100%; max-width: 820px; max-height: 90vh; overflow: auto; transform: translateY(8px) scale(.99); transition: transform .2s; }
.pmodal-scrim.is-open .pmodal { transform: none; }
.pmodal__head { display: flex; align-items: center; gap: 12px; padding: 18px 22px; border-bottom: 1px solid var(--border-subtle); position: sticky; top: 0; background: var(--color-bg); z-index: 2; }
.pmodal__head h3 { font-size: 19px; margin: 0; }
.pmodal__close { margin-left: auto; width: 34px; height: 34px; border: 1px solid var(--border-default); background: #fff; border-radius: var(--radius-sm); cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text-muted); }
.pmodal__close:hover { background: var(--lss-ink-100); }
.pmodal__close svg { width: 18px; height: 18px; }
.pmodal__body { padding: 24px; }

/* MODAL_FOOTER_OVERFLOW_2026-08-01 — modal footer actions.
   These were inline `display:flex;justify-content:flex-end` with no wrapping.
   When the buttons are wider than the dialog, flex-end pushes the overflow off
   the START edge: on a phone the first button is clipped by the left edge of
   the screen, where it can be neither read nor tapped. Wrapping fixes the
   desktop case; the stack in the 820px block below fixes the phone case. */
.pmodal__actions { display: flex; gap: 10px; justify-content: flex-end; align-items: center; flex-wrap: wrap; }
/* Two classes deep on purpose: components.css has `form.inline { margin:0 }`
   at (0,1,1), which outranks a lone class and would silently kill the
   auto margin that pins the destructive button to the left. */
.pmodal__actions .pmodal__actions-start { margin-right: auto; }

/* drawer (course detail from calendar) */
.pscrim { position: fixed; inset: 0; background: rgba(16,14,46,.45); z-index: 60; opacity: 0; pointer-events: none; transition: opacity .2s; }
.pscrim.is-open { opacity: 1; pointer-events: auto; }
/* height cascade (2026-07-17 UX_UI_AUDIT): iOS Safari 100vh doesn't shrink for
   the URL bar, so the drawer footer (Close/Claim/Enroll) rendered behind
   Safari chrome — same bug class fixed for .psb in NAV_DRAWER_MOBILE_SCROLL. */
.pdrawer { position: fixed; top: 0; right: 0; width: 420px; max-width: 92vw; height: 100vh; height: 100dvh; height: 100svh; background: #fff; z-index: 70; box-shadow: var(--shadow-xl); transform: translateX(100%); transition: transform .28s var(--ease-out); display: flex; flex-direction: column; padding-bottom: env(safe-area-inset-bottom, 0px); }
.pdrawer.is-open { transform: none; }
.pdrawer__head { padding: 0; border-bottom: 1px solid var(--border-subtle); }
.pdrawer__hero { padding: 22px; color: #fff; position: relative; }
.pdrawer__hero h3 { font-family: var(--font-display); font-weight: 800; text-transform: uppercase; font-size: 24px; margin: 8px 0 4px; color: #fff; letter-spacing: -.01em; }
.pdrawer__hero .close { position: absolute; right: 16px; top: 16px; width: 30px; height: 30px; border: 0; background: rgba(255,255,255,.18); color: #fff; border-radius: var(--radius-sm); cursor: pointer; display: flex; align-items: center; justify-content: center; }
.pdrawer__hero .close svg { width: 16px; height: 16px; }
.pdrawer__body { padding: 22px; flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 18px; }
/* Column, not row — the footer stacks full-width block actions (staffing
   note, claim/enroll forms, poster link, Close). As a row they overflowed
   the 420px drawer and clipped the Close button (fixed 2026-07-04). */
.pdrawer__foot { padding: 16px 22px; border-top: 1px solid var(--border-subtle); display: flex; flex-direction: column; gap: 10px; }

/* DRAWER_FOOT_MOBILE_2026-09-15 — Andrew: "drawer buttons on mobile cover up a
   lot of the details of the course on a phone."

   The foot does not overlay the body — .pdrawer is a flex column and they are
   siblings — but stacking every action full width grows it downward until the
   body has nothing left. Measured in Chromium at 390x700, a phone viewport
   with browser chrome, against the real markup:

       admin          foot 293px, course details 296px
       local admin    foot 483px, course details 106px

   Two-up below 640px, with the actions that must stay full width named
   individually rather than by position, so adding a button later does not
   silently change which ones pair.

   minmax(0, 1fr), not 1fr: the intrinsic width of "Reschedule course"
   otherwise wins and the columns come out 220/158. Same reason
   .roster-row--pair uses minmax.

   After: admin 209/380, local admin 315/274. */
@media (max-width: 640px) {
  .pdrawer__foot {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 8px;
    align-content: start;
    /* The grid alone fixes the admin case. A local admin still reaches 395px,
       because three of that role's items are full width below — so cap it and
       let it scroll rather than let the course disappear again. Never engages
       for an admin. */
    max-height: 45dvh;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  /* .lss-btn is white-space: nowrap — right for a full-width button, wrong for
     a half-width one. Scoped to the mobile foot so nothing else changes. */
  .pdrawer__foot .lss-btn { white-space: normal; line-height: 1.2; text-align: center; }

  /* Full width, and why:
       staffing / noreg    not buttons — a meter and a sentence
       claim/enroll/       the PRIMARY action for whoever is looking, and
       register/waitlist   register carries the policy checkbox with it
       uncancel            only ever appears on its own
       delete              must not sit beside Cancel under the same thumb */
  .pdrawer__foot > [data-d-staffing],
  .pdrawer__foot > [data-d-action="claim"],
  .pdrawer__foot > [data-d-action="enroll"],
  .pdrawer__foot > [data-d-action="register"],
  .pdrawer__foot > [data-d-action="waitlist"],
  .pdrawer__foot > [data-d-action="noreg"],
  .pdrawer__foot > [data-d-action="uncancel"],
  .pdrawer__foot > [data-d-action="delete"] { grid-column: 1 / -1; }
}
.detail-line { display: flex; align-items: center; gap: 12px; }
.detail-line__ic { width: 36px; height: 36px; border-radius: var(--radius-md); background: var(--lss-navy-50); display: flex; align-items: center; justify-content: center; flex: none; color: var(--lss-navy-600); }
.detail-line__ic svg { width: 17px; height: 17px; }
.detail-line b { font-size: 14.5px; color: var(--text-strong); display: block; }
.detail-line span { font-size: 12.5px; color: var(--text-muted); }
.capacity-bar { height: 8px; border-radius: var(--radius-pill); background: var(--lss-ink-100); overflow: hidden; }
.capacity-bar i { display: block; height: 100%; background: var(--lss-orange-500); border-radius: var(--radius-pill); }
.session-chips { display: flex; flex-direction: column; gap: 6px; margin: -6px 0 2px 48px; }
.session-chip { display: flex; align-items: center; gap: 9px; font-size: 13px; color: var(--text-body); font-family: var(--font-mono); }
.session-chip b { width: 20px; height: 20px; border-radius: 50%; background: var(--lss-navy-100); color: var(--lss-navy-700); display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; font-family: var(--font-sans); flex: none; }

/* misc */
.empty { text-align: center; color: var(--text-muted); padding: 50px 20px; }
.empty img { width: 46px; opacity: .4; margin-bottom: 12px; }
.row-actions { display: flex; gap: 8px; justify-content: flex-end; }
.checkbox-cell { display: flex; align-items: center; }
.toast-host { position: fixed; right: 24px; bottom: 24px; z-index: 120; width: 390px; max-width: calc(100vw - 48px); }
/* TABS_WRAP_2026-08-06 (site audit) — the admin Settings page has 13 tabs
   needing ~1550px in one row. Below 820px a media rule makes the row
   scrollable, but between 821px and ~1550px there was NO handling: the
   row is nowrap by default, so the last tabs ("Mail ↗", "Notifications &
   push ↗", "Wallet ↗") hung past the right edge of the page — measured
   222px of page-level horizontal overflow at a 1320px window. Wrapping at
   desktop widths keeps every tab visible and clickable; the ≤820px block
   further down still switches to the scroll treatment on phones. */
.tabs { display: flex; flex-wrap: wrap; gap: 4px; border-bottom: 1px solid var(--border-subtle); margin-bottom: 20px; }
.tab { padding: 11px 16px; border: 0; background: none; font-family: var(--font-sans); font-weight: 600; font-size: 14px; color: var(--text-muted); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; }
.tab:hover { color: var(--lss-navy-700); }
.tab.is-active { color: var(--lss-navy-700); border-bottom-color: var(--lss-orange-500); }

@media (max-width: 1080px) {
  .login { grid-template-columns: 1fr; }
  .login__brand { display: none; }
  .builder, .shift-grid { grid-template-columns: 1fr; }
  .builder--form { grid-template-columns: 1fr; }
  .pkpis { grid-template-columns: repeat(2, 1fr); }
  /* MEDIA_ORDER_2026-08-09 — .dash-2col, .online-grid and .loc-grid USED to be
     declared here and were silently dead. Their base rules live further down
     this file (887, 941, 897); a media query adds no specificity, so the later
     base rule won every time and these three never collapsed. Moved to a second
     1080px block placed AFTER those base rules — see MEDIA_ORDER_2026-08-09
     below. Anything added to THIS block must have its base rule above line 761
     or it will be dead on arrival too. */
}

/* ---------- Notification bell + panel ---------- */
.nbell { position: relative; }
.nbell__btn { position: relative; width: 40px; height: 40px; border: 1px solid var(--border-default); background: #fff; border-radius: var(--radius-md); cursor: pointer; color: var(--lss-navy-700); display: flex; align-items: center; justify-content: center; }
.nbell__btn:hover { background: var(--lss-ink-50); }
.nbell__btn svg { width: 19px; height: 19px; }
.nbell__dot { position: absolute; top: -6px; right: -6px; min-width: 18px; height: 18px; padding: 0 5px; border-radius: var(--radius-pill); background: var(--lss-orange-500); color: #fff; font-size: 11px; font-weight: 700; display: flex; align-items: center; justify-content: center; border: 2px solid #fff; }
.npanel { position: absolute; right: 0; top: calc(100% + 8px); width: 360px; max-width: 86vw; background: #fff; border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); box-shadow: var(--shadow-xl); z-index: 90; overflow: hidden; }
.npanel__head { display: flex; align-items: center; padding: 13px 16px; border-bottom: 1px solid var(--border-subtle); }
.npanel__head b { font-size: 14px; color: var(--text-strong); }
.npanel__mark { margin-left: auto; background: none; border: 0; color: var(--lss-navy-600); font-weight: 600; font-size: 12.5px; cursor: pointer; font-family: var(--font-sans); }
/* 2026-07-19 (npanel__mark hover bounce): the earlier rule flipped padding
   0 -> 2px 6px + margin 0 -> -2px -6px on hover to add a subtle chip. The
   base rule (line 701 above) has zero padding, but the SECOND rule at
   line 1451 (post-a11y sweep) applies padding: 6px 8px; min-height: 32px
   without a media query — so on hover, padding SHRANK from 6px 8px to
   2px 6px + negative margin, visibly bouncing the button. Fix: keep
   hover to purely visual state (background + color); rely on line 1451
   for the base padding/border-radius. No box-model change on hover. */
.npanel__mark:hover { background-color: var(--hover-surface); color: var(--lss-navy-800, #191742); }
/* Explicit close (X) in the panel head. Small on desktop (secondary
   affordance next to Mark all read); tap-target sized on mobile where
   it's the primary way out of a full-viewport panel. */
.npanel__close { margin-left: 8px; background: none; border: 0; padding: 6px; cursor: pointer;
                 color: var(--text-muted); border-radius: var(--radius-sm);
                 display: inline-flex; align-items: center; justify-content: center; }
.npanel__close:hover { color: var(--text-strong); background: var(--lss-ink-50); }
.npanel__close svg { width: 18px; height: 18px; display: block; }
.npanel__list { max-height: 360px; overflow-y: auto; }
.npanel__empty { text-align: center; color: var(--text-muted); padding: 36px 20px; }
.npanel__empty img { width: 38px; opacity: .4; margin-bottom: 10px; }
.npanel__empty p { font-size: 13px; margin: 0; }
.nitem { display: flex; gap: 11px; align-items: flex-start; width: 100%; text-align: left; padding: 12px 16px; border: 0; border-bottom: 1px solid var(--border-subtle); background: none; cursor: pointer; font-family: var(--font-sans); position: relative; }
.nitem:hover { background: var(--lss-ink-50); }
.nitem.is-unread { background: var(--lss-orange-50); }
.nitem.is-unread:hover { background: var(--lss-orange-100); }
.nitem__ic { width: 32px; height: 32px; border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center; flex: none; }
.nitem__ic svg { width: 16px; height: 16px; }
.nitem__title { display: block; font-size: 13.5px; font-weight: 600; color: var(--text-strong); line-height: 1.3; }
.nitem__text { display: block; font-size: 12.5px; color: var(--text-muted); line-height: 1.4; margin-top: 2px; }
/* Deep-link affordance — surfaces the CTA label so the user can preview
   where the row will take them. Folded into aria-label for AT, so the
   visible CTA is decorative (aria-hidden in markup).
   NOTIF_ACTIONS_2026-09-27: superseded in the bell by the .nbtn buttons
   below; kept because broadcast.php's preview card still renders a row. */
.nitem__cta { display: inline-block; margin-top: 6px; font-size: 12px; font-weight: 600; color: var(--lss-navy-700); letter-spacing: 0.01em; }
.nitem--link:hover .nitem__cta { color: var(--lss-orange-600); }

/* NOTIF_ACTIONS_2026-09-27 — real buttons on a notification row.
   The row is a submit button and a button may not contain a button or a
   link, so the actions are siblings of it inside .nitem-wrap. The wrap
   therefore has to take over the tint, the hover and the divider, or the
   strip would sit outside the row it belongs to. .nitem keeps its own
   copies of those for broadcast.php, which renders a bare row with no
   wrapper. The left padding lines the buttons up with the row's text:
   16px panel + 32px icon + 11px gap. */
.nitem-wrap { border-bottom: 1px solid var(--border-subtle); }
.nitem-wrap.is-unread { background: var(--lss-orange-50); }
.nitem-wrap:hover { background: var(--lss-ink-50); }
.nitem-wrap.is-unread:hover { background: var(--lss-orange-100); }
.nitem-wrap .nitem { border-bottom: 0; padding-bottom: 7px; }
.nitem-wrap .nitem:hover,
.nitem-wrap .nitem.is-unread { background: none; }
.nitem__acts { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 16px 12px 59px; }
.nitem__acts form { display: inline-flex; }
.nbtn {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 12px; min-height: 30px;
  border: 1px solid var(--border-default); border-radius: 15px;
  background: #fff; cursor: pointer;
  font-family: var(--font-sans); font-size: 12px; font-weight: 600;
  color: var(--lss-navy-700); text-decoration: none; white-space: nowrap;
}
.nbtn:hover { border-color: var(--lss-orange-400); color: var(--lss-orange-600); }
.nbtn--ghost { background: transparent; }
.nitem__unread { position: absolute; right: 12px; top: 16px; width: 7px; height: 7px; border-radius: 50%; background: var(--lss-orange-500); }
.npanel__foot { display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; padding: 12px; border: 0; background: var(--lss-ink-50); cursor: pointer; font-family: var(--font-sans); font-weight: 600; font-size: 13px; color: var(--lss-navy-700); }
.npanel__foot:hover { background: var(--lss-ink-100); }
.npanel__foot svg { width: 15px; height: 15px; }

/* ---------- Toggle switch + preference rows ---------- */
.toggle { width: 42px; height: 24px; border-radius: var(--radius-pill); border: 0; background: var(--lss-ink-300); cursor: pointer; position: relative; flex: none; transition: background var(--dur-fast) var(--ease-standard); padding: 0; }
/* --toggle-on: per-instance override so the calendar visibility toggles can
   inherit the colour of the event type they control. Default keeps every
   other toggle in the portal on the canonical success green. */
.toggle.is-on { background: var(--toggle-on, var(--lss-success)); }
.toggle__knob { position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: var(--shadow-sm); transition: transform var(--dur-fast) var(--ease-out); }
.toggle.is-on .toggle__knob { transform: translateX(18px); }
.pref-row { display: flex; align-items: center; gap: 16px; padding: 14px 0; border-bottom: 1px solid var(--border-subtle); }
.pref-row:last-child { border-bottom: 0; }
.pref-row > div { flex: 1; }
.pref-row b { display: block; font-size: 14px; color: var(--text-strong); }
.pref-row span { display: block; font-size: 12.5px; color: var(--text-muted); margin-top: 2px; }

/* ---------- Calendar visibility panel (iOS-style slider filters) ----------
   Each row is a colour-coded toggle the user clicks to show/hide an event
   "kind" on the calendar grid. Auto-saves per-user via api/calendar-prefs.php.
   Mobile collapses to one column; desktop is a 2-col grid when there are
   more than 4 rows. */
/* Calendar visibility panel — collapsible <details>.
   Closed state is a single-row summary so the calendar grid stays high in
   the viewport; opens to reveal the full toggle list + bulk actions. */
.cal-vis { margin-bottom: 18px; background: #fff; border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); overflow: hidden; }
.cal-vis > summary {
    display: flex; align-items: center; gap: 10px;
    padding: 10px 16px;
    cursor: pointer; user-select: none;
    list-style: none;          /* hide the default disclosure triangle */
    min-height: 44px;          /* tap target on mobile */
}
.cal-vis > summary::-webkit-details-marker { display: none; }
.cal-vis > summary:focus-visible { outline: 3px solid var(--lss-orange-500); outline-offset: -3px; }
.cal-vis > summary:hover { background: var(--lss-ink-50, #f5f6fa); }
.cal-vis__caret { color: var(--text-muted); transition: transform .15s ease; flex: none; }
.cal-vis[open] .cal-vis__caret { transform: rotate(90deg); }
.cal-vis__title { font-size: 13px; font-weight: 700; color: var(--text-strong); letter-spacing: .02em; text-transform: uppercase; }
.cal-vis__count {
    font-size: 12px; font-weight: 600; color: var(--text-muted);
    background: var(--lss-ink-100, #eef0f4); padding: 3px 9px; border-radius: 99px;
    white-space: nowrap;
}
.cal-vis__saved { margin-left: auto; font-size: 12px; color: var(--lss-success); display: inline-flex; align-items: center; gap: 4px; opacity: 0; transition: opacity .2s; }
.cal-vis__saved.is-shown { opacity: 1; }
.cal-vis__body { padding: 4px 18px 12px; border-top: 1px solid var(--border-subtle); }
.cal-vis__quick { display: flex; gap: 8px; padding: 12px 0 6px; }
.cal-vis__quick button { font: 600 12px/1 var(--font-sans); padding: 8px 12px; border-radius: var(--radius-sm); border: 1px solid var(--border-default); background: #fff; color: var(--lss-navy-700); cursor: pointer; min-height: 36px; }
.cal-vis__quick button:hover { background: var(--lss-ink-50); }
.cal-vis__grid { display: grid; grid-template-columns: 1fr; gap: 0; }
.cal-vis__row { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--border-subtle); }
.cal-vis__row:last-child { border-bottom: 0; }
.cal-vis__row .cal-vis__sw { width: 10px; height: 10px; border-radius: 3px; flex: none; }
.cal-vis__row > label { flex: 1; font-size: 14px; color: var(--text-strong); cursor: pointer; user-select: none; }
.cal-vis__row .toggle { margin-left: auto; }

/* DEPT_SWITCHES_2026-10-02 — Andrew: "I prefer toggle switches to checkmarks."
   The portal's switch is .toggle/.toggle__knob, but every existing one is a
   <button> that posts on click. These sit inside forms with a single Save, so
   the control has to be a checkbox. Rather than invent a second switch, the
   checkbox is visually hidden and a .toggle span beside it mirrors :checked
   through a sibling selector — identical pixels to the switches in the rows
   above, no JavaScript, and the keyboard still drives the real input. */
.pm-sw { display:flex; align-items:center; gap:16px; padding:11px 0; cursor:pointer;
         border-bottom:1px solid var(--border-subtle,#eef0f3); margin:0; }
.pm-sw:last-of-type { border-bottom:0; }
.pm-sw > input { position:absolute; width:1px; height:1px; opacity:0; margin:0; }
.pm-sw__txt { flex:1; min-width:0; }
.pm-sw__txt b { display:block; font-size:14px; color:var(--text-strong); font-weight:600; }
.pm-sw__txt span { display:block; font-size:12.5px; color:var(--text-muted); margin-top:2px; line-height:1.45; }
.pm-sw .toggle { pointer-events:none; }   /* the label is the hit target */
.pm-sw > input:checked ~ .toggle { background:var(--toggle-on, var(--lss-success)); }
.pm-sw > input:checked ~ .toggle .toggle__knob { transform:translateX(18px); }
.pm-sw > input:focus-visible ~ .toggle { outline:2px solid var(--lss-navy-500,#232160); outline-offset:2px; }
.pm-sub { font-size:12.5px; font-weight:700; letter-spacing:.03em; text-transform:uppercase;
          color:#6a6d80; margin:16px 0 2px; }
.pm-ship { display:flex; align-items:center; gap:10px; margin:14px 0 2px; }
.pm-ship label { font-size:13.5px; color:var(--text-strong); font-weight:600; }
/* DEPT_RECIPIENTS_CHIPS_2026-10-02 — a list of who is on it plus one small
   box for the next one, instead of a textarea you had to know the separator
   rules for. */
.pm-rcpt { margin:2px 0 0; }
.pm-rcpt__list { display:flex; flex-wrap:wrap; gap:6px; margin:0 0 9px; }
.pm-rcpt__chip { display:inline-flex; align-items:center; gap:6px; padding:5px 6px 5px 11px;
                 border-radius:999px; background:#eef0fa; border:1px solid #d7dcf0;
                 font-size:12.5px; color:#232160; font-weight:600; max-width:100%; }
.pm-rcpt__chip > span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.pm-rcpt__chip button { border:0; background:none; cursor:pointer; color:#5b6080; font-size:15px;
                        line-height:1; padding:0 2px; min-width:22px; min-height:22px; border-radius:50%; }
.pm-rcpt__chip button:hover { color:#a23222; background:#e2e5f5; }
.pm-rcpt__empty { font-size:12.5px; color:var(--text-muted); font-style:italic; }
.pm-rcpt__add { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.pm-rcpt__add .lss-input { width:260px; max-width:100%; }
.pm-rcpt__hint { font-size:12px; color:var(--text-faint,#8a8d9c); }

@media (min-width: 720px) {
  .cal-vis__grid { grid-template-columns: 1fr 1fr; column-gap: 24px; }
  .cal-vis__row { border-bottom: 1px solid var(--border-subtle); }
  .cal-vis__row:nth-last-child(-n+2) { border-bottom: 0; }
}
@media (max-width: 519px) {
  .cal-vis > summary { flex-wrap: wrap; padding: 10px 14px; }
  .cal-vis__quick { width: 100%; }
  .cal-vis__quick button { flex: 1; }
}

/* ---------- Dashboards (2-col), demand bars ---------- */
.dash-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }

/* ---- MOBILE_FIELD_GRID_2026-09-13 -------------------------------------
   Two-up form fields that become one-up on a phone.

   Settings -> Security wrote this grid inline nine times, and an inline
   grid-template-columns cannot be collapsed by a media query — which is how
   "Grace period (days)" ended up a few characters wide on Andrew's phone with
   its hint breaking mid-word down the side. `1fr` is `minmax(auto, 1fr)`, so
   the two tracks stay equal only until one child needs more than its share.

   __full spans the row with `1 / -1` rather than `span 2` on purpose: on the
   one-column phone layout `span 2` does not clamp, it invents a second
   implicit column and undoes the collapse. `1 / -1` is the whole row whatever
   the track count, so no media query and no !important are needed to correct
   it. ---- */
.lss-fgrid       { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.lss-fgrid--3    { grid-template-columns: 1fr 1fr 1fr; }
.lss-fgrid__full { grid-column: 1 / -1; }
@media (max-width: 768px) {
  .lss-fgrid, .lss-fgrid--3 { grid-template-columns: 1fr; }
}

/* location overview cards */
/* LOC_GRID_MINWIDTH_2026-08-06 (site audit) — same disease as
   GRID_MINWIDTH_2026-07-27: a bare 1fr track's minimum is the item's
   MIN-CONTENT width, so one organization card with a wide unbreakable
   line pushed its whole column wider than the grid and the card painted
   past the panel edge (measured 75px past the viewport on the dashboard).
   minmax(0,1fr) removes the content floor; min-width:0 on the card lets
   its ellipsised children actually shrink. */
.loc-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; margin-bottom: 22px; }
.loc-card { min-width: 0; background: #fff; border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); box-shadow: var(--shadow-xs); padding: 16px; cursor: pointer; transition: box-shadow .15s, transform .12s, border-color .15s; display: flex; flex-direction: column; gap: 12px; }
.loc-card:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); border-color: var(--border-default); }
.loc-card__top { display: flex; align-items: center; gap: 10px; }
.loc-card__pin { width: 34px; height: 34px; border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; color: #fff; flex: none; }
.loc-card__pin svg { width: 17px; height: 17px; }
.loc-card__name { font-size: 14px; color: var(--text-strong); display: block; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.loc-card__city { font-size: 12px; color: var(--text-muted); }
.loc-card__sector { margin-top: -4px; }
.loc-card__stats { display: flex; gap: 14px; }
.loc-card__stats > div { display: flex; flex-direction: column; }
.loc-card__stats b { font-family: var(--font-display); font-weight: 800; font-size: 20px; color: var(--text-strong); line-height: 1; }
.loc-card__stats span { font-size: 11px; color: var(--text-muted); margin-top: 3px; }
.loc-card__comp { height: 6px; border-radius: var(--radius-pill); background: var(--lss-ink-100); overflow: hidden; }
.loc-card__comp i { display: block; height: 100%; border-radius: var(--radius-pill); }
.loc-card__admin { font-size: 12.5px; }
.loc-card__has { display: flex; align-items: center; gap: 8px; color: var(--text-body); font-weight: 500; }
.loc-card__av { width: 24px; height: 24px; border-radius: 50%; background: var(--lss-navy-100); color: var(--lss-navy-700); display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 700; flex: none; }
.loc-card__needs { display: flex; align-items: center; gap: 7px; color: var(--lss-warning); font-weight: 600; }
.loc-card__needs svg { width: 15px; height: 15px; }
.demand-row { display: flex; flex-direction: column; gap: 7px; }
/* DEMAND_ROW_WRAP_2026-08-09 — Andrew, from an iPhone: the "3 staff need
   this · none scheduled" line rendered one CHARACTER per line beside the
   Schedule button.

   Both of these rows were non-wrapping flex with space-between, and a flex
   item's default min-width is auto for text but the BUTTON refuses to
   shrink — so in a narrow container the label absorbed every pixel of the
   squeeze until it was a column of single letters. The dash-2col grid does
   collapse to one column below 1080px, so this should not bite on a phone;
   it did, which means a stale cached stylesheet was in play (SW_VERSION is
   bumped alongside this change). Either way the row should not depend on
   its container being generous — wrap it, let the label keep its own line,
   and stop the button shrinking. */
.demand-row__top { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.demand-row__top > span:first-child { flex: 1 1 auto; min-width: 0; }
.demand-row__top > span:last-child  { flex: 0 0 auto; }
.demand-row__bar { height: 8px; border-radius: var(--radius-pill); background: var(--lss-ink-100); overflow: hidden; }
.demand-row__bar i { display: block; height: 100%; border-radius: var(--radius-pill); }
.demand-row__foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.demand-row__foot > span   { flex: 1 1 12rem; min-width: 0; }
.demand-row__foot > .lss-btn { flex: 0 0 auto; white-space: nowrap; }

/* ---------- Online courses + player ---------- */
.online-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }

/* MEDIA_ORDER_2026-08-09 — the real home for the 1080px collapses whose base
   rules are declared below the original block at line 761.

   Andrew, from an iPhone: Course demand and Needs an instructor sat SIDE BY
   SIDE on a phone, squeezing the demand rows into a 174px column. The cause
   was not the demand row at all — .dash-2col was declared 1fr 1fr at line 887,
   after the max-width:1080px rule that was supposed to collapse it. Equal
   specificity, later wins, so the phone rule never applied at any width.

   Reproduced at a 390px viewport in headless Chromium: grid-template-columns
   computed to "174px 174px" before this fix and "366px" after.

   .online-grid and .loc-grid had the same defect but were masked, because the
   narrower phone block near the end of this file re-states them — so they only
   misbehaved between 820px and 1080px (tablets), where nothing else caught it.
   All three are fixed here together. */
@media (max-width: 1080px) {
  .dash-2col  { grid-template-columns: 1fr; }
  .online-grid { grid-template-columns: 1fr; }
  .loc-grid   { grid-template-columns: repeat(2, minmax(0, 1fr)); } /* LOC_GRID_MINWIDTH_2026-08-06 */
}
/* 2026-07-18 (ONLINE_TAB_FOCUS_2026-07-17.md): the whole card is now
   keyboard-tabbable + click-anywhere, not just the small button in the
   footer. Stretched-link pattern — .ocard is positioned, and the inner
   Start/Resume/Review anchor's ::before pseudo covers the entire card via
   position:absolute inset:0 — so a click anywhere on the card fires the
   inner link's href, AND a Tab landing on the inner <a> propagates focus
   up to the card via :focus-within, painting a full-card outline instead
   of a tiny ring around a footer button. Rewrite is CSS-only; the PHP
   markup keeps its existing .ocard > .ocard__foot > <a class="lss-btn">
   structure so every render path (progress + completed + not-started
   states) inherits the fix without a branch-by-branch edit. */
.ocard { background: #fff; border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); overflow: hidden; display: flex; flex-direction: column; position: relative; transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease; }
.ocard:hover { border-color: var(--lss-orange-400, #ff8a48); box-shadow: var(--shadow-md, 0 6px 16px rgba(0,0,0,.09)); transform: translateY(-1px); }
.ocard:focus-within { outline: 3px solid var(--lss-orange-500); outline-offset: 2px; border-color: var(--lss-orange-500); }
/* Stretched link — the primary anchor covers the whole card via ::before.
   The visible button stays where it is; the ::before is invisible but
   click-catching, so a click anywhere on the card fires the anchor's
   href. Text selection inside the card is disabled by the overlay (minor
   a11y trade for click-anywhere UX); the card body is short prose, and
   users needing to copy text can right-click "Copy link" or Tab to the
   focused card and let the a11y tree read it.

   CARD_STRETCH_SCOPE_2026-09-11 — this selector used to be
   `a.lss-btn`, which matched EVERY link in the footer rather than the one
   that is meant to own the card. Admins and local admins get a second footer
   link (Poster), rendered after Start, so its overlay painted on top of
   Start's and swallowed the whole card: clicking Start course — or anywhere
   on the tile — opened course-poster.php, and because that link is
   target="_blank" the page in front of you did nothing at all. Verified in a
   browser: admin hit course-poster.php, staff hit ?learn=. Learners never saw
   it, because they have no Poster link.

   Scoped to an explicit class now. Adding another footer link cannot steal
   the card again, and a state that forgets the class loses click-anywhere
   (visibly, on its own card) rather than silently redirecting the whole tile
   somewhere else. */
.ocard .ocard__foot > a.ocard__go::before { content: ""; position: absolute; inset: 0; z-index: 1; }
/* The overlay covers the footer too, which means it also covers the OTHER
   controls in it — so Poster (and the Buy form) have to be lifted above it or
   they become unclickable. Scoping the overlay alone would have swapped one
   dead button for another: verified in a browser, Start worked and Poster
   stopped responding until this rule was added. Anything interactive that
   shares the footer needs to sit above the overlay, which is why this is
   written as :not(.ocard__go) rather than naming Poster. */
.ocard .ocard__foot > a.lss-btn:not(.ocard__go),
.ocard .ocard__foot > form,
.ocard .ocard__foot > button { position: relative; z-index: 2; }
/* Same stretched-link + focus-within pattern for the LIST view — scoped
   to .online-catalog--list so the shared .ptable__row class on other
   pages (staff / registry / dashboard tables) is not affected. */
.online-catalog--list .ptable__row:not(.ptable__head) { position: relative; transition: background-color .15s ease; }
.online-catalog--list .ptable__row:not(.ptable__head):hover { background-color: var(--lss-navy-50); }
.online-catalog--list .ptable__row:not(.ptable__head):focus-within { outline: 3px solid var(--lss-orange-500); outline-offset: -3px; background-color: var(--lss-navy-50); z-index: 1; }
/* CARD_STRETCH_SCOPE_2026-09-11 — same scoping as the grid above. The list
   view was not actually broken, but only by luck of source order: Poster is
   rendered BEFORE the Start/Resume link here, so the right overlay happened
   to be on top. Reordering the row would have broken it silently. Make it
   deliberate rather than lucky. */
.online-catalog--list .ptable__row .row-actions > a.row-go::before { content: ""; position: absolute; inset: 0; z-index: 1; }
/* Same lift as the grid — the row overlay covers the row's own buttons. */
.online-catalog--list .ptable__row .row-actions > a.lss-btn:not(.row-go),
.online-catalog--list .ptable__row .row-actions > form,
.online-catalog--list .ptable__row .row-actions > button { position: relative; z-index: 2; }
.ocard__top { height: 96px; position: relative; display: flex; align-items: center; justify-content: center; }
.ocard__top img { width: 40px; height: 40px; }
.ocard__play { position: absolute; right: 12px; bottom: 12px; background: rgba(255,255,255,.92); color: var(--lss-navy-700); font-size: 11px; font-weight: 700; padding: 4px 9px; border-radius: var(--radius-pill); display: flex; align-items: center; gap: 5px; }
.ocard__body { padding: 16px 18px; flex: 1; }
.ocard__body h3 { font-size: 17px; margin: 0 0 6px; }
.ocard__meta { display: flex; gap: 12px; color: var(--text-muted); font-size: 12.5px; margin-top: 10px; }
.ocard__meta span { display: inline-flex; align-items: center; gap: 5px; }
.ocard__meta svg { width: 13px; height: 13px; }
.ocard__foot { padding: 13px 18px; border-top: 1px solid var(--border-subtle); display: flex; align-items: center; gap: 10px; }
.oprogress { height: 6px; border-radius: var(--radius-pill); background: var(--lss-ink-100); overflow: hidden; flex: 1; }
.oprogress i { display: block; height: 100%; background: var(--lss-success); border-radius: var(--radius-pill); }

.player { display: grid; grid-template-columns: 260px 1fr; gap: 22px; align-items: start; }
.player__rail { background: #fff; border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); overflow: hidden; position: sticky; top: 84px; }
.player__rail-head { padding: 16px 18px; border-bottom: 1px solid var(--border-subtle); }
.player__rail-head b { font-size: 15px; color: var(--text-strong); }
.player__rail-head span { display: block; font-size: 12px; color: var(--text-muted); margin-top: 3px; }
.mstep { display: flex; align-items: center; gap: 11px; padding: 12px 18px; border: 0; background: none; width: 100%; text-align: left; cursor: pointer; font-family: var(--font-sans); border-bottom: 1px solid var(--border-subtle); }
.mstep:hover { background: var(--lss-ink-50); }
.mstep.is-active { background: var(--lss-navy-50); }
.mstep__num { width: 24px; height: 24px; border-radius: 50%; border: 1.5px solid var(--border-strong); display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; color: var(--text-muted); flex: none; }
.mstep.done .mstep__num { background: var(--lss-success); border-color: var(--lss-success); color: #fff; }
.mstep.is-active .mstep__num { border-color: var(--lss-navy-600); color: var(--lss-navy-700); }
.mstep__t { font-size: 13.5px; color: var(--text-strong); font-weight: 500; }
.mstep.done .mstep__t { color: var(--text-muted); }
.mstep__num svg { width: 13px; height: 13px; }
.player__main { background: #fff; border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); padding: 30px 34px; min-height: 420px; display: flex; flex-direction: column; }
.player__lesson h2 { font-size: 26px; margin: 0 0 6px; }
.player__lesson .eyebrow { font-weight: 700; font-size: 12px; letter-spacing: .12em; text-transform: uppercase; color: var(--text-accent); margin-bottom: 10px; }
.player__lesson p { color: var(--text-body); line-height: 1.7; margin: 0 0 14px; max-width: 60ch; }
.player__media { background: var(--lss-gradient-navy); border-radius: var(--radius-md); aspect-ratio: 16/8; display: flex; align-items: center; justify-content: center; margin: 6px 0 20px; position: relative; }
.player__media img { width: 54px; opacity: .9; }
.player__media .ptag { position: absolute; left: 14px; top: 12px; }
.player__foot { margin-top: auto; padding-top: 22px; border-top: 1px solid var(--border-subtle); display: flex; align-items: center; gap: 10px; }
.quiz-opt { display: flex; align-items: center; gap: 11px; padding: 13px 15px; border: 1px solid var(--border-default); border-radius: var(--radius-md); cursor: pointer; margin-bottom: 10px; font-size: 14.5px; color: var(--text-strong); transition: border-color .12s, background .12s; }
.quiz-opt:hover { border-color: var(--lss-navy-300); }
.quiz-opt.is-pick { border-color: var(--lss-orange-500); background: var(--lss-orange-50); }
.quiz-opt__mk { width: 20px; height: 20px; border-radius: 50%; border: 1.5px solid var(--border-strong); flex: none; }
.quiz-opt.is-pick .quiz-opt__mk { border-color: var(--lss-orange-500); background: var(--lss-orange-500); box-shadow: inset 0 0 0 3px #fff; }
.player__done { text-align: center; padding: 30px 20px; }
.player__done img { width: 60px; margin-bottom: 16px; }
.player__done h2 { font-size: 28px; margin: 0 0 8px; }
.player__done p { color: var(--text-muted); max-width: 40ch; margin: 0 auto 22px; }

/* ---------- Billing ---------- */
.bill-grid { display: grid; grid-template-columns: 1.5fr 1fr; gap: 18px; align-items: start; }
.plan-card { background: var(--lss-navy-700); color: #fff; border-radius: var(--radius-lg); padding: 22px; position: relative; overflow: hidden; }
.plan-card::after { content:""; position:absolute; right:-30px; top:-30px; width:160px; height:160px; background: radial-gradient(circle, rgba(239,106,46,.4), transparent 65%); }
.plan-card .eyebrow { font-weight: 700; font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--lss-orange-300); position: relative; }
.plan-card h3 { font-size: 24px; color: #fff; margin: 6px 0 2px; position: relative; }
.plan-card p { color: #c7c6e0; font-size: 13px; margin: 0; position: relative; }
.plan-card__price { font-family: var(--font-display); font-weight: 800; font-size: 34px; margin-top: 16px; position: relative; }
.plan-card__price small { font-size: 14px; font-weight: 500; color: #c7c6e0; font-family: var(--font-sans); }
.seat-bar { height: 9px; border-radius: var(--radius-pill); background: rgba(255,255,255,.18); overflow: hidden; margin-top: 14px; position: relative; }
.seat-bar i { display: block; height: 100%; background: var(--lss-orange-500); border-radius: var(--radius-pill); }
.pay-method { display: flex; align-items: center; gap: 13px; padding: 14px; border: 1px solid var(--border-subtle); border-radius: var(--radius-md); }
.pay-method__brand { width: 46px; height: 32px; border-radius: var(--radius-xs); background: var(--lss-navy-700); color: #fff; display: flex; align-items: center; justify-content: center; font-family: var(--font-display); font-weight: 800; font-size: 11px; letter-spacing: .04em; flex: none; }
.inv-row { display: grid; grid-template-columns: 130px 1fr 110px 110px auto; align-items: center; gap: 14px; padding: 13px 20px; border-bottom: 1px solid var(--border-subtle); }
.inv-row:last-child { border-bottom: 0; }
.inv-mono { font-family: var(--font-mono); font-size: 13px; color: var(--text-body); }
@media (max-width: 1080px) { .player { grid-template-columns: 1fr; } .bill-grid { grid-template-columns: 1fr; } }

/* ============================================================
   Layout-driven certificates (.certx) — designer + render path
   ============================================================ */
.certx { display: block; padding: 0; text-align: left; }
.certx .cx { position: absolute; box-sizing: border-box; }
/* Defensive overflow clamps — text, image and QR slots must never bleed past
   their declared area, even when the recipient name, course title or address
   is unexpectedly long.  Per-element max-height is added inline by the
   renderer based on the gap to the next element below (see cert_render_el). */
.certx .cx-text { white-space: pre-wrap; overflow-wrap: break-word; word-break: break-word; overflow: hidden; }
.certx .cx-img { overflow: hidden; }
.certx .cx-img img { width: 100%; height: 100%; display: block; object-fit: contain; max-width: 100%; max-height: 100%; }
.certx .cx-qr { background: #fff; border-radius: 2px; overflow: hidden; }
.certx .cx-qr img, .certx .cx-qr canvas { position: absolute; left: 4%; top: 4%; width: 92% !important; height: 92% !important; }
.certx .cx-qr__fb { font-family: var(--font-mono); font-size: .9cqw; color: var(--text-faint); word-break: break-all; }

/* Multi-day course sessions on the calendar */
.cal__event--multi { border-left: 3px solid rgba(255,255,255,.65); }
.cal__event-day { font-family: var(--font-mono); font-size: 9.5px; font-weight: 700; background: rgba(255,255,255,.28); padding: 0 4px; border-radius: 3px; margin-left: 3px; }

/* Upload dropzone */
.dropzone { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; text-align: center; padding: 26px 18px; border: 1.5px dashed var(--border-default); border-radius: 12px; background: #fafbfd; cursor: pointer; transition: border-color .15s, background .15s; }
.dropzone:hover { border-color: var(--lss-navy-700); background: #f4f6fb; }
.dropzone.is-drag { border-color: var(--lss-orange-500); background: var(--lss-orange-100); }
.dropzone.has-file { border-style: solid; border-color: var(--lss-success); background: var(--lss-success-bg); }
.dropzone__icon { width: 30px; height: 30px; display: inline-flex; color: var(--lss-navy-700); }
.dropzone__icon svg { width: 100%; height: 100%; }
.dropzone__main, .dropzone__hint { display: block; width: 100%; }
.dropzone.has-file .dropzone__icon { color: var(--lss-success); }
.dropzone__main { font-size: 14px; font-weight: 700; color: var(--text-strong); word-break: break-all; }
.dropzone__hint { font-size: 12px; color: var(--text-faint); }

/* Instructor qualification summary chips + manage modal */
/* QCHIP_NAME_FONT_2026-08-15 — was var(--font-mono). The mono face suited
   course codes and status tokens but read wrong on the instructor-name pills
   (open-shifts "Qualified to teach" panel). Andrew liked the sans face there
   and asked for it on every qchip, so the base rule now uses the UI sans
   font; size/weight are unchanged so the dense code-chip lanes keep their
   fit. */
.qchip { font-family: var(--font-sans); font-size: 11px; font-weight: 700; color: var(--lss-navy-700); background: var(--lss-navy-100); border-radius: 5px; padding: 2px 7px; }
.qchip--more { background: transparent; color: var(--text-faint); }
/* CSS_GAPS_2026-09-19 — public-user.php's consent table asked for
   `qchip badge--ok` / `badge--warn`. There is no .badge in this codebase, so
   "granted" and "withdrawn" rendered identically. Same tint tokens as
   .lss-badge, named with the house words. */
.qchip--success { background: var(--lss-success-bg); color: var(--lss-success); }
.qchip--warning { background: var(--lss-warning-bg); color: #9a6406; }
.qcat-label { font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--text-faint); margin: 0 0 8px; }

/* Rich-text editor (lesson authoring) */
.rte { border: 1px solid var(--border-default); border-radius: var(--radius-sm); overflow: hidden; background: #fff; }
.rte__bar { display: flex; flex-wrap: wrap; gap: 2px; padding: 6px; border-bottom: 1px solid var(--border-subtle); background: #fafbfd; }
.rte__bar button { width: 30px; height: 30px; border: 0; background: transparent; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 700; color: var(--lss-navy-700); font-family: var(--font-sans); }
.rte__bar button:hover { background: var(--lss-navy-100); }
.rte__bar .rte-sep { width: 1px; background: var(--border-default); margin: 4px 3px; }
.rte__area { min-height: 180px; max-height: 420px; overflow: auto; padding: 12px 14px; font-size: 14px; line-height: 1.6; color: var(--text-body); outline: none; }
.rte__area:empty:before { content: attr(data-placeholder); color: var(--text-faint); }
.rte__area p { margin: 0 0 10px; }
.rte__area h3 { font-size: 17px; margin: 4px 0 8px; color: var(--text-strong); }
.rte__area h4 { font-size: 15px; margin: 4px 0 6px; color: var(--text-strong); }
.rte__area ul, .rte__area ol { margin: 0 0 10px; padding-left: 22px; }
.rte__area a { color: var(--lss-navy-700); }

/* Compact styled file picker */
.filepick { display: inline-flex; align-items: center; gap: 12px; cursor: pointer; }
.filepick input[type=file] { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.filepick__btn { display: inline-flex; align-items: center; gap: 7px; padding: 9px 15px; border: 1px solid var(--border-default); border-radius: 9px; background: #fff; font-weight: 700; font-size: 13px; color: var(--lss-navy-700); white-space: nowrap; }
.filepick:hover .filepick__btn { border-color: var(--lss-navy-700); }
.filepick__btn svg { width: 15px; height: 15px; }
.filepick__name { font-size: 13px; color: var(--text-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.filepick.has-file .filepick__name { color: var(--text-strong); font-weight: 600; }

/* Course notes callout (course drawer) */
.course-notes { background: var(--lss-orange-100, #fdeee6); border: 1px solid color-mix(in srgb, var(--lss-orange-500, #ef6a2e) 28%, #fff); border-left: 3px solid var(--lss-orange-500, #ef6a2e); border-radius: 10px; padding: 12px 14px; margin-top: 4px; }
.course-notes__head { display: flex; align-items: center; gap: 7px; font-size: 11px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase; color: var(--lss-orange-700, #8f3210); margin-bottom: 6px; }
.course-notes__ic { width: 14px; height: 14px; display: inline-flex; }
.course-notes__ic svg { width: 100%; height: 100%; }
.course-notes__body { font-size: 13.5px; line-height: 1.55; color: var(--text-body); white-space: pre-line; }

/* ============================================================
   Accessibility primitives (WCAG 2.1 AA target)
   - Used site-wide (sr-only, skip-link, focus-visible defaults)
     and by the online course player.
   ============================================================ */

/* Screen-reader-only — visually hidden but still announced. */
.sr-only {
  position: absolute !important;
  width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* Skip link — invisible until focused, brought into view on top-left. */
.skip-link {
  position: absolute; left: 8px; top: -42px;
  background: var(--lss-navy-900, #1a1840); color: #fff;
  padding: 9px 16px; border-radius: var(--radius-md, 8px);
  font-weight: 700; font-size: 14px; text-decoration: none;
  z-index: 100; transition: top 120ms var(--ease-standard, ease);
}
.skip-link:focus, .skip-link:focus-visible {
  top: 8px; outline: 3px solid var(--lss-orange-500, #ef6a2e); outline-offset: 2px;
}

/* Default keyboard-focus indicator for any non-customised focusable.
   Components that paint their own focus ring (buttons, inputs, checks)
   already win via their own :focus-visible rules — this is the safety net. */
a:focus-visible,
button:focus-visible,
[role="button"]:focus-visible,
[tabindex]:not([tabindex="-1"]):focus-visible {
  outline: 3px solid var(--lss-orange-500, #ef6a2e);
  outline-offset: 2px;
  border-radius: 4px;
}
/* Removing outlines without replacement is forbidden — flag in review.
   The few :focus rules above use box-shadow instead, which is also fine. */

/* Respect prefers-reduced-motion across the portal. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------- Online-course player — semantic landmarks ---------- */
.player-exit { margin: 0 0 8px; }
.player-exit__link {
  display: inline-flex; align-items: center; gap: 4px;
  color: var(--lss-navy-700); text-decoration: none; font-weight: 600;
  padding: 6px 4px; border-radius: 6px; min-height: 32px;
}
.player-exit__link:hover { background-color: var(--hover-surface); color: var(--lss-navy-800, #191742); }
.player-exit__link:focus-visible {
  outline: 3px solid var(--lss-orange-500); outline-offset: 2px;
}
.player-exit__link svg { width: 18px; height: 18px; }

/* Course-title header for the player (h1 now, was h2). Visual size matches
   the rest of the portal's h2 so nothing looks oversized; semantic weight
   reflects "this is the page's primary heading." */
.phead--player .phead__title {
  font-size: 26px; margin: 0; font-weight: 800; color: var(--text-strong);
  line-height: 1.2; letter-spacing: -0.005em;
}
.phead--player .phead__meta {
  margin: 6px 0 0; color: var(--text-muted); font-size: 13.5px;
}

/* Stepper rail h2 — sits inside <nav aria-label="Module steps">. */
.player__rail-title {
  font-size: 15px; margin: 0; color: var(--text-strong); font-weight: 700;
}
.player__rail-progress {
  margin: 3px 0 8px; font-size: 12px; color: var(--text-muted);
}
.player__rail-bar {
  display: block; height: 6px; border-radius: var(--radius-pill);
  background: var(--lss-ink-100); overflow: hidden;
}
.player__rail-bar i {
  display: block; height: 100%; background: var(--lss-success);
  border-radius: var(--radius-pill);
  transition: width var(--dur-base) var(--ease-standard);
}

.mstep-list { list-style: none; margin: 0; padding: 0; }
.mstep-item { margin: 0; }

/* Stepper items — keyboard-visible focus + WCAG 2.5.5 hit target. */
.mstep {
  text-decoration: none; min-height: 44px; /* 44×44 touch target */
}
.mstep:focus-visible {
  outline: 3px solid var(--lss-orange-500); outline-offset: -3px;
  background: var(--lss-navy-50);
  z-index: 1; position: relative;
}
.mstep[aria-current="step"] { background: var(--lss-navy-50); }
.mstep[aria-current="step"] .mstep__num {
  border-color: var(--lss-navy-600); color: var(--lss-navy-700);
}

/* Player main pane — programmatic focus target on step change. */
.player__main:focus { outline: none; }
/* Subtle ring when programmatically focused via tabindex=-1, but only when
   coming from keyboard (focus-visible is reliable on modern browsers).
   Landed 2026-07-17 UX_UI_AUDIT — the promised-in-comment focus indicator was
   never actually written, leaving keyboard/AT users with no cue on step advance. */
.player__main:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 3px rgba(232,90,50,.45);
  border-radius: 6px;
}
.player__form { display: flex; flex-direction: column; flex: 1; min-width: 0; }

/* Lesson typography — use rem-friendly sizes so 200% zoom still works. */
.player__lesson { display: flex; flex-direction: column; gap: 0; }
.player__lesson h2 { font-size: 1.65rem; line-height: 1.25; }
.player__lesson .lesson-body { font-size: 1rem; line-height: 1.65; }

/* Media wrappers — replace inline styles for consistent layout. */
.player__figure { margin: 0 0 14px; }
.player__video {
  width: 100%; max-height: 62vh; border-radius: 12px;
  background: #000; display: block;
}
.player__audio { width: 100%; margin: 14px 0; display: block; }
.player__image {
  width: 100%; max-height: 62vh; object-fit: contain;
  border-radius: 12px; display: block; background: var(--lss-ink-50);
}
.player__iframe {
  width: 100%; height: 64vh; border: 0;
  border-radius: 12px; background: #fff; display: block;
}
.player__doc-note { font-size: 0.85rem; color: var(--text-muted); margin-top: 6px; }
.player__caption-note {
  font-size: 0.82rem; color: var(--text-muted); margin: 8px 0 0;
  padding: 8px 12px; background: var(--lss-warning-bg, #fff5d9);
  border-left: 3px solid var(--lss-warning, #d99a06);
  border-radius: 4px;
}
.player__caption-note a { color: var(--lss-navy-700); font-weight: 600; }

/* Footer + actions — buttons keep WCAG 2.5.5 size via .lss-btn--md (42 high). */
.player__duration {
  font-size: 0.85rem; color: var(--text-muted);
  display: inline-flex; align-items: center; gap: 6px;
}
.player__duration-ic { width: 14px; height: 14px; display: inline-flex; }
.player__foot-actions { margin-left: auto; display: flex; gap: 10px; flex-wrap: wrap; }

/* ---------- Knowledge-check (quiz) fieldset ---------- */
.quiz-fs { border: 0; padding: 0; margin: 18px 0 0; }
.quiz-fs__legend {
  font-size: 1.55rem; font-weight: 800; color: var(--text-strong);
  line-height: 1.3; padding: 0; margin: 0 0 6px;
}
.quiz-fs__required { color: var(--lss-emergency); margin-left: 4px; }
.quiz-fs__hint { font-size: 0.85rem; color: var(--text-muted); margin: 0 0 14px; }
.quiz-list { list-style: none; margin: 0; padding: 0; }
.quiz-list__item { margin: 0 0 10px; }

/* Re-defining .quiz-opt to layer focus / selection states without color-only
   signal — selection gets a check-mark icon AND a fill change AND a label
   weight change, so a color-blind learner sees the same affordance. */
.quiz-opt {
  position: relative; display: flex; align-items: center; gap: 12px;
  padding: 13px 16px; border: 1.5px solid var(--border-default);
  border-radius: var(--radius-md); cursor: pointer;
  font-size: 1rem; color: var(--text-strong); min-height: 44px;
  transition: border-color var(--dur-fast) var(--ease-standard),
              background var(--dur-fast) var(--ease-standard),
              box-shadow var(--dur-fast) var(--ease-standard);
}
.quiz-opt:hover { border-color: var(--lss-navy-300); background: var(--lss-ink-50); }
.quiz-opt:has(input:checked) {
  border-color: var(--lss-orange-500);
  background: var(--lss-orange-50);
}
.quiz-opt__input {
  /* Visually hidden but focusable — sr-only pattern. */
  position: absolute; width: 1px; height: 1px; padding: 0;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
  margin: 0;
}
.quiz-opt__mk {
  flex: none; width: 22px; height: 22px; border-radius: 50%;
  border: 1.5px solid var(--border-strong); background: #fff;
  display: inline-flex; align-items: center; justify-content: center;
  transition: border-color var(--dur-fast), background var(--dur-fast),
              box-shadow var(--dur-fast);
}
.quiz-opt__check { width: 12px; height: 12px; color: #fff; opacity: 0; transform: scale(0.6); transition: opacity var(--dur-fast), transform var(--dur-fast); }
.quiz-opt__check svg { width: 100%; height: 100%; display: block; }
.quiz-opt__text { line-height: 1.4; }

.quiz-opt__input:checked ~ .quiz-opt__mk {
  border-color: var(--lss-orange-600);
  background: var(--lss-orange-500);
}
.quiz-opt__input:checked ~ .quiz-opt__mk .quiz-opt__check {
  opacity: 1; transform: scale(1);
}
.quiz-opt__input:checked ~ .quiz-opt__text {
  font-weight: 700; color: var(--lss-navy-900, var(--text-strong));
}
/* Keyboard focus indicator — visible halo on the visible mark + label. */
.quiz-opt__input:focus-visible ~ .quiz-opt__mk {
  box-shadow: 0 0 0 3px var(--lss-orange-300);
}
.quiz-opt:has(.quiz-opt__input:focus-visible) {
  outline: 3px solid var(--lss-orange-500);
  outline-offset: 2px;
}

/* Footnote when a11y toggle is on. */
.player__a11y-footnote {
  margin: 18px 0 0; font-size: 0.8rem; color: var(--text-muted);
  background: var(--lss-info-bg, #eaf2ff); border-radius: 6px;
  padding: 8px 12px; border-left: 3px solid var(--lss-info, #2b6cb0);
}
.player__a11y-footnote kbd {
  font-family: var(--font-mono); font-size: 0.78rem;
  padding: 1px 5px; border-radius: 3px;
  background: #fff; border: 1px solid var(--border-default);
  color: var(--text-strong);
}
.player__a11y-footnote a {
  color: var(--lss-navy-700); font-weight: 600;
}

/* ---------- Authenticated page footer ----------
   Three-column layout on desktop: brand mark + copyright on the left,
   utility nav in the middle, trust chips on the right. Stacks vertically
   below 720px so it doesn't crowd a phone viewport. */
.papp-footer {
  /* 2026-07-12: horizontal geometry mirrors .pcontent (26px gutters,
     1680px cap, centered) so the footer aligns with the content above
     instead of hugging the viewport edges. */
  margin: 36px auto 0; padding: 18px 26px 22px;
  max-width: 1680px; width: 100%; box-sizing: border-box;
  border-top: 1px solid var(--border-subtle);
  display: flex; flex-wrap: wrap; gap: 14px 28px;
  align-items: center; justify-content: space-between;
  font-size: 12.5px; color: var(--text-muted);
}
@media (max-width: 1023px) {
  .papp-footer { padding-left: 18px; padding-right: 18px; } /* match .pcontent tablet */
}
.papp-footer__brand {
  display: inline-flex; align-items: center; gap: 10px;
  font-weight: 500;
}
.papp-footer__mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px;
  background: var(--lss-navy-700); color: #fff;
  border-radius: 6px;
  font: 700 11px/1 var(--font-sans);
  letter-spacing: 0.5px;
}
.papp-footer__copy { color: var(--text-muted); }
.papp-footer__nav {
  display: inline-flex; flex-wrap: wrap; gap: 4px 6px;
  align-items: center;
}
.papp-footer__link {
  display: inline-flex; align-items: center; gap: 7px;
  color: var(--text-muted); text-decoration: none;
  padding: 6px 10px; min-height: 32px;
  border-radius: 999px;
  transition: background .12s, color .12s;
}
.papp-footer__link:hover,
.papp-footer__link:focus-visible {
  color: var(--lss-navy-700);
  background: var(--lss-ink-100, #f3f4f7);
  text-decoration: none;
}
.papp-footer__link:focus-visible {
  outline: 2px solid var(--lss-orange-500); outline-offset: 2px;
}
.papp-footer__link-ic { display: inline-flex; opacity: .7; }
.papp-footer__link-ic svg { width: 13px; height: 13px; }
.papp-footer__link:hover .papp-footer__link-ic { opacity: 1; }
.papp-footer__trust {
  display: inline-flex; flex-wrap: wrap; gap: 8px;
  color: var(--text-faint);
}
.papp-footer__chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 10px;
  border: 1px solid var(--border-subtle);
  border-radius: 999px;
  font: 600 10.5px/1.4 var(--font-sans);
  letter-spacing: 0.3px;
  color: var(--text-faint);
  background: #fff;
}
.papp-footer__chip svg { width: 12px; height: 12px; opacity: .8; }
@media (max-width: 720px) {
  .papp-footer { gap: 12px; padding: 16px 14px 20px; } /* match .pcontent phone */
  .papp-footer__brand,
  .papp-footer__nav,
  .papp-footer__trust { width: 100%; }
  .papp-footer__nav { gap: 2px; }
}

/* ---------- Public page footer ----------
   A little more prominent than the authenticated one — there's a brand block
   with tagline up top, link cluster, contact email, then a bottom bar with
   the copyright and trust chips. Used on enrollment / verify / catalog. */
.pubfoot {
  margin-top: 56px;
  background: var(--lss-navy-50, #f5f6fc);
  border-top: 1px solid var(--border-subtle);
}
.pubfoot__inner {
  max-width: 1200px; margin: 0 auto;
  padding: 28px 24px 22px;
  display: grid; gap: 18px 32px;
  grid-template-columns: 1.4fr 1.4fr 1fr;
  align-items: start;
}
.pubfoot__brand {
  display: flex; align-items: center; gap: 12px;
  font-size: 14px; color: var(--text-strong);
}
.pubfoot__mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px;
  background: var(--lss-navy-700); color: #fff;
  border-radius: 9px;
  font: 700 14px/1 var(--font-sans);
  letter-spacing: 0.8px;
  flex: none;
}
.pubfoot__tagline {
  display: block;
  font: 400 12.5px/1.45 var(--font-sans);
  color: var(--text-muted);
  margin-top: 2px;
}
.pubfoot__nav {
  display: flex; flex-direction: column; gap: 4px;
}
.pubfoot__link {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 6px 10px; margin-left: -10px;
  border-radius: 6px;
  font-size: 13px; color: var(--text-body, #2d3142); text-decoration: none;
  width: fit-content; min-height: 32px;
  transition: background .12s, color .12s;
}
.pubfoot__link:hover,
.pubfoot__link:focus-visible {
  color: var(--lss-navy-700); background: #fff;
  text-decoration: none;
}
.pubfoot__link:focus-visible {
  outline: 2px solid var(--lss-orange-500); outline-offset: 2px;
}
.pubfoot__link-ic { display: inline-flex; opacity: .65; }
.pubfoot__link-ic svg { width: 14px; height: 14px; }
.pubfoot__link:hover .pubfoot__link-ic { opacity: 1; }
.pubfoot__contact {
  display: flex; flex-direction: column; gap: 4px;
  font-size: 13px; color: var(--text-muted);
}
.pubfoot__contact-label {
  font-weight: 600; color: var(--text-strong); font-size: 12.5px;
}
.pubfoot__contact a {
  color: var(--lss-navy-700); text-decoration: none;
  word-break: break-all;
}
.pubfoot__contact a:hover { text-decoration: underline; }
.pubfoot__bottom {
  max-width: 1200px; margin: 0 auto;
  padding: 12px 24px 18px;
  border-top: 1px solid var(--border-subtle);
  display: flex; flex-wrap: wrap; gap: 10px 18px;
  align-items: center; justify-content: space-between;
  font-size: 11.5px; color: var(--text-faint);
}
.pubfoot__trust {
  display: inline-flex; flex-wrap: wrap; gap: 8px;
}
.pubfoot__chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 10px;
  border: 1px solid var(--border-subtle);
  border-radius: 999px;
  font: 600 10.5px/1.4 var(--font-sans);
  letter-spacing: 0.3px;
  color: var(--text-faint);
  background: #fff;
}
.pubfoot__chip svg { width: 12px; height: 12px; opacity: .8; }
@media (max-width: 880px) {
  .pubfoot__inner { grid-template-columns: 1fr; gap: 18px; padding: 24px 18px 18px; }
  .pubfoot__bottom { padding: 12px 18px 16px; }
}

/* Information not by color alone: status pills that only used color now
   carry a leading symbol-character for color-blind / mono displays. */
.lss-badge--success::before { content: "\2713  "; /* check */ font-weight: 800; }
.lss-badge--emergency::before { content: "\26A0  "; /* warning triangle */ }
.lss-badge--warning::before { content: "\26A0  "; }

/* Phone breakpoint sanity — ensure stepper + main both stack with full
   keyboard order preserved and 200% zoom doesn't clip. */
@media (max-width: 720px) {
  .player__rail { position: static; }
  .phead--player .phead__title { font-size: 1.35rem; }
  .player__lesson h2, .quiz-fs__legend { font-size: 1.25rem; }
  .player__foot { flex-wrap: wrap; }
}

/* Accessibility statement page — typographic adjustments. */
.a11y-doc { max-width: 760px; line-height: 1.7; }
.a11y-doc h2 { margin-top: 1.6em; }
.a11y-doc ul { padding-left: 22px; }
.a11y-doc dl { display: grid; grid-template-columns: minmax(140px, 200px) 1fr; gap: 6px 18px; }
.a11y-doc dt { font-weight: 700; color: var(--text-strong); }
.a11y-doc dd { margin: 0; color: var(--text-body); }

/* ============================================================
   Portal-wide WCAG 2.1 AA enhancements (extended from course player)
   ============================================================ */

/* Sidebar nav — ensure focus ring is visible against dark bg + 44px min target */
.psb__item, .psb__user { min-height: 44px; }
.psb__item:focus-visible {
  outline: 3px solid var(--lss-orange-400);
  outline-offset: -3px; background: rgba(255,255,255,.10); color: #fff;
}
.psb__user:focus-visible {
  outline: 3px solid var(--lss-orange-400); outline-offset: -3px;
  background: rgba(255,255,255,.15);
}

/* Topbar role label — used to be a misused <label>, now a semantic span */
.ptb__role-label {
  font-size: 11px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-faint); white-space: nowrap;
}

/* Notification bell — 44x44 target + clear focus ring */
.nbell__btn { width: 44px; height: 44px; }
.nbell__btn:focus-visible {
  outline: 3px solid var(--lss-orange-500); outline-offset: 2px;
}
.nitem { min-height: 44px; }
.nitem:focus-visible {
  outline: 3px solid var(--lss-orange-500); outline-offset: -3px;
  background: var(--lss-orange-50);
}
.npanel__mark { padding: 6px 8px; min-height: 32px; border-radius: var(--radius-sm); }
.npanel__mark:focus-visible {
  outline: 2px solid var(--lss-orange-500); outline-offset: 1px;
}

/* Calendar event button — focus visible against colored bg */
.cal__event:focus-visible {
  outline: 3px solid #fff; outline-offset: -3px;
  box-shadow: 0 0 0 5px var(--lss-navy-700);
}
.cal__nav a:focus-visible, .cal__nav button:focus-visible {
  outline: 3px solid var(--lss-orange-500); outline-offset: 2px;
}

/* Tabs — WAI-ARIA pattern, focus ring on visible label */
.tab { min-height: 44px; }
.tab:focus-visible {
  outline: 3px solid var(--lss-orange-500); outline-offset: -3px;
  background: var(--lss-navy-50); border-radius: 4px 4px 0 0;
}

/* Modal — close button focus ring */
.pmodal__close { min-width: 34px; min-height: 34px; }
.pmodal__close:focus-visible {
  outline: 3px solid var(--lss-orange-500); outline-offset: 2px;
}
.pdrawer__hero .close { min-width: 32px; min-height: 32px; }
.pdrawer__hero .close:focus-visible {
  outline: 3px solid #fff; outline-offset: 2px; box-shadow: 0 0 0 5px var(--lss-navy-700);
}

/* Doc-chip / qual-chip / filter chips — 44px target + visible focus ring */
.doc-chip, .qual-chip, .filter-chip { min-height: 36px; }
.doc-chip:focus-visible, .qual-chip:focus-visible, .filter-chip:focus-visible,
.aud-card:focus-visible {
  outline: 3px solid var(--lss-orange-500); outline-offset: 2px;
}

/* Doc-row clickable rows + role buttons — focus ring */
.role-btn:focus-visible, .att-session:focus-visible {
  outline: 3px solid var(--lss-orange-500); outline-offset: 2px;
}

/* Toggle switch — focus ring on the visible track when wrapped <input> focused */
.toggle:has(input:focus-visible),
.toggle:focus-visible {
  outline: 3px solid var(--lss-orange-500); outline-offset: 2px;
}

/* Toolbar buttons in cert builder / lists — visible focus on icon buttons */
.icon-btn:focus-visible {
  outline: 2px solid var(--lss-orange-500); outline-offset: 1px;
}

/* Att-toggle + result-toggle — color-blind safety: keep the leading check/×
   icons (they already have them in markup); just add a focus ring. */
.att-toggle button:focus-visible,
.result-toggle button:focus-visible {
  outline: 3px solid var(--lss-orange-500); outline-offset: -3px;
  z-index: 1; position: relative;
}

/* Loc-card / pkpi cards — focus ring */
.loc-card:focus-visible {
  outline: 3px solid var(--lss-orange-500); outline-offset: 2px;
  text-decoration: none;
}

/* lss-btn fallback — every button gets a focus ring even if no class match.
   The course-player pass already covers .lss-btn via components.css. */
button:not([class*="lss-btn"]):not(.toggle):not(.cal__event):not(.psb__item):not(.psb__user):not(.nbell__btn):not(.nitem):not(.tab):not(.pmodal__close):not(.doc-chip):not(.qual-chip):not(.filter-chip):not(.aud-card):not(.role-btn):not(.att-session):not(.att-toggle):not(.result-toggle):not(.icon-btn):not(.npanel__mark):not(.mstep):not(.swatch-btn):not(.quiz-opt):focus-visible {
  outline: 3px solid var(--lss-orange-500); outline-offset: 2px;
}

/* Inputs / selects / textareas — make sure the focus ring on the *visible*
   wrapper (.lss-input, .lss-select) is loud enough. Existing rules already
   handle .lss-input:focus-within; this is a safety net for unwrapped inputs.
   2026-07-18 (DOUBLE_BORDER_TEXTAREA_2026-07-17.md): textarea selector was
   unqualified, so any textarea wrapped in a .lss-input div (user-data-erase
   "Reason", many form-render textareas, admin comment boxes) fired BOTH the
   .lss-input:focus-within ring AND this rule — sighted users saw a soft
   rounded outer ring PLUS a sharp inner 3px outline — a "double border" on
   focus. Excluded via the same :not(.lss-input textarea) pattern the input
   + select selectors already use. Standalone textareas (no wrapper) keep
   the safety-net outline.
   2026-07-18 (AUDIT_RESULT_DROPDOWN_FOCUS_2026-07-18.md): same bug on selects.
   The select safety-net only excluded .lss-select-wrapped selects, so any
   <select> inside a .lss-input div (Audit → Outcome on form-submission.php,
   Bulk outcome on form-submissions.php, and every other <div class="lss-input">
   <select> in the portal) fired BOTH rings on focus. Added the matching
   :not(.lss-input select) exclusion; also excluded bare select.lss-input so
   the one-class pattern (see components.css DOUBLE_BORDER_FIX_2026-07-19)
   doesn't double-ring either. */
input:not(.lss-input input):not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):focus-visible,
select:not(.lss-select select):not(.lss-input select):not(select.lss-input):focus-visible,
textarea:not(.lss-input textarea):focus-visible {
  outline: 3px solid var(--lss-orange-500); outline-offset: 0;
}

/* Forms — visible required marker that doesn't rely on a parent style */
.req-mark { color: var(--lss-emergency); margin-left: 2px; }
.req-mark::before { content: "*"; }

/* Validation error chip below an input — used by aria-describedby */
.field-error {
  display: flex; align-items: center; gap: 6px;
  margin-top: 6px; font-size: 12.5px; font-weight: 600;
  color: var(--lss-emergency);
}
.field-error::before {
  content: "\26A0";  /* warning triangle — icon + text, not color alone */
  font-size: 13px; line-height: 1;
}

/* Status pills that historically used only color now also carry a leading
   symbol (info/danger/etc). The course-player pass added success/warning/
   emergency.

   INFO_BADGE_PREFIX_2026-07-27: the matching `.lss-badge--info::before
   { content: "i  " }` rule is removed. It was the odd one out — its
   siblings above emit real glyphs (\2713, \26A0) that stand in for the
   colour, whereas this one printed a literal lowercase letter, so the
   sector pill on the dashboard and the location header read "i Industry".
   Nothing is lost by dropping it: the info tone is never used solid, and
   `.lss-badge--info` / `--navy` tints are commented out in components.css
   (@dead audit-2026-07-17 — that comment is stale, there are ~10 live info
   call sites), so the pill was already carrying its meaning in its own
   text rather than in colour. The two dot-variant call sites
   (badge($short, 'info', false, true)) keep their leading dot. */

/* Standing badge (staff cert status) — add icon for color-blind safety */
.standing__dot { box-shadow: 0 0 0 1px rgba(0,0,0,.12); }
.standing--active::before { content: "\2713  "; color: var(--lss-success); font-weight: 800; }
.standing--expiring::before { content: "\26A0  "; color: var(--lss-warning); }
.standing--expired::before { content: "\2715  "; color: var(--lss-emergency); font-weight: 800; }
.standing--missing::before { content: "\2014  "; color: var(--text-muted); }

/* Skip-to-section links — used on multi-section pages (Dashboard / Settings) */
.skip-section-link {
  position: absolute; left: -9999px; top: 0;
  background: var(--lss-navy-900); color: #fff;
  padding: 8px 14px; border-radius: var(--radius-md);
  font-weight: 600; font-size: 13px; text-decoration: none; z-index: 50;
}
.skip-section-link:focus, .skip-section-link:focus-visible {
  left: 8px; outline: 3px solid var(--lss-orange-500); outline-offset: 2px;
}

/* Sub-skip block: when multiple skip-section links are needed, render them
   in a row that's only visible on focus-within. */
.skip-section-row {
  position: absolute; top: 8px; left: 8px; display: flex; gap: 6px;
  z-index: 60; pointer-events: none;
}
.skip-section-row > a { pointer-events: auto; }

/* Caption for data tables (screen readers describe table purpose) */
.ptable__caption,
.tbl-caption {
  font-size: 13px; font-weight: 600; color: var(--text-strong);
  text-align: left; padding: 12px 20px;
  border-bottom: 1px solid var(--border-subtle);
}
.tbl-caption--sr { /* visually-hidden caption when there's a visible h2/h3 */
  position: absolute !important;
  width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* Page-level main content — ensure it gets a visible focus ring when the
   skip-link sends focus into it. */
.pcontent:focus-visible { outline: 3px solid var(--lss-orange-500); outline-offset: -3px; border-radius: 4px; }

/* Touch target safety for compact mini-select (used in doc-row, etc) */
.mini-select { min-height: 32px; }
.mini-select:focus-visible {
  outline: 2px solid var(--lss-orange-500); outline-offset: 1px;
}

/* Topbar role chip — sometimes wraps a label-like element. Make sure it
   doesn't behave as a form label since we now use a span. */
.ptb__role { min-height: 36px; }

/* ============================================================
   MOBILE RESPONSIVENESS (Pass 1 — Foundation chrome)

   Breakpoints:
   - >=1024px : Desktop (untouched — left as-is per design)
   - 821-1023 : Tablet (sidebar still visible, density tightened by
                pre-existing 1080px breakpoint above)
   - <=820px  : Phone (sidebar collapses to drawer; hamburger appears)

   Why 820px and not 768? Because the sidebar+main+gutter math becomes
   uncomfortable around 820, and instructors taking attendance on a
   landscape phone (~812-844 wide) still expect the drawer pattern.

   Hamburger nav follows the QR App's pattern:
   - aria-controls / aria-expanded on the button
   - Esc / scrim tap / link selection / outside-tap closes
   - Focus trap when open, focus restored to hamburger on close
   ============================================================ */

/* Hamburger button — hidden on desktop, revealed at <=820px */
.ptb__hamburger {
  display: none;
  width: 44px; height: 44px;
  flex: none;
  border: 1px solid var(--border-default);
  background: #fff;
  border-radius: var(--radius-md);
  color: var(--lss-navy-700);
  cursor: pointer;
  align-items: center;
  justify-content: center;
  padding: 0;
  margin-right: 4px;
}
.ptb__hamburger:hover { background: var(--lss-ink-50); }
.ptb__hamburger svg { width: 22px; height: 22px; }
.ptb__hamburger:focus-visible {
  outline: 3px solid var(--lss-orange-500);
  outline-offset: 2px;
}

/* Sidebar drawer scrim (mobile only) — sits over the page when sidebar
   is open as drawer. Brand-color tint matches QR app. */
.psb-scrim {
  display: none;
  position: fixed; inset: 0;
  background: rgba(35, 33, 96, 0.55);  /* brand navy at 55% */
  z-index: 65;
  opacity: 0;
  transition: opacity var(--dur-base, .2s) var(--ease-standard, ease);
}
.psb-scrim.is-open {
  display: block;
  opacity: 1;
}

/* Sticky topbar always — iOS notch padding via env() */
.ptb {
  position: sticky; top: 0; z-index: 30;
  padding-top: max(0px, env(safe-area-inset-top));
}

/* Tablet (821–1023) — keep sidebar but ensure topbar is sticky and content
   density slightly tighter. Pre-existing 1080 breakpoint already collapses
   the multi-column dashboards. */
@media (max-width: 1023px) {
  .pcontent { padding: 20px 18px; }
  .phead h1, .phead h2 { font-size: 22px; }
}

/* PHONE BREAKPOINT — <= 820px ----------------------------------- */
@media (max-width: 820px) {

  /* Hamburger appears, sidebar becomes a left-slide drawer. */
  .ptb__hamburger { display: inline-flex; }

  /* App layout flattens — sidebar leaves grid flow. */
  .papp { grid-template-columns: 1fr; }

  /* Sidebar transforms into off-canvas drawer. We position it fixed (not
     sticky) and translate it offscreen until .is-open is toggled.

     iOS Safari note (fixed 2026-07-16, NAV_DRAWER_MOBILE_SCROLL): the
     drawer used `height: 100vh` (via a mis-ordered dvh/vh fallback pair
     that let vh win on every modern browser). 100vh on iOS Safari does
     NOT shrink when the URL bar is visible, so the bottom items lived
     BEHIND Safari's chrome — the profile row + Sign Out were cut off
     with no way to scroll to them. The cascade below gives modern
     browsers the small-viewport-height (URL bar visible = worst case),
     guaranteeing every item is always reachable. `bottom: 0` +
     `overflow-y: auto` on the flex column lets the inner content
     scroll to the safe-area edge on top of that. */
  .psb {
    position: fixed;
    top: 0; left: 0;
    bottom: 0;
    width: 86vw; max-width: 320px;
    height: 100vh;   /* ultimate fallback for browsers without dvh/svh */
    height: 100dvh;  /* dynamic viewport — shrinks with URL bar */
    height: 100svh;  /* small viewport — always fits smallest chrome state */
    transform: translateX(-100%);
    transition: transform 0.28s var(--ease-out, cubic-bezier(.2,.7,.3,1));
    z-index: 70;
    box-shadow: var(--shadow-xl, 0 20px 50px rgba(0,0,0,.35));
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    padding-top: max(20px, env(safe-area-inset-top));
    /* Extra breathing room past the safe-area so the last item (profile
       row / Sign out) isn't flush against the home indicator. */
    padding-bottom: calc(max(20px, env(safe-area-inset-bottom, 0px)) + 24px);
  }
  .psb.is-open { transform: translateX(0); }

  /* Make sidebar items finger-friendly on mobile */
  .psb__item { padding: 12px 14px; font-size: 15px; }
  .psb__user { padding: 12px 12px; }

  /* Topbar density on phone */
  .ptb {
    height: 56px;
    padding: 0 14px;
    padding-top: max(0px, env(safe-area-inset-top));
    gap: 10px;
  }
  .ptb__crumb { font-size: 13px; gap: 6px; min-width: 0; }
  .ptb__crumb-page { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .ptb__right { gap: 6px; margin-left: auto; }

  /* Hide the explicit role chip on phone — the crumb already shows it */
  .ptb__role { display: none; }

  /* Collapse the "View as:" role switcher to just its shield icon + caret on
     phone — the full "View as: Local Administrator" label overflowed the
     topbar next to the bell + Sign out. The dropdown still opens and shows
     the active role, so nothing is lost. */
  .ptb__rs .ptb__rs-text { display: none; }
  .ptb__rs .js-rs-toggle { padding: 6px 8px; }

  /* Make "Sign out" tighter on phone */
  .ptb__right .lss-btn--sm { padding: 6px 10px; }

  /* Content gutters tighten on phone */
  .pcontent { padding: 16px 14px; }

  /* Page header stacks naturally — actions wrap underneath the heading */
  .phead { gap: 12px; margin-bottom: 16px; }
  .phead h1, .phead h2 { font-size: 20px; }
  .phead p { font-size: 13.5px; }
  .phead__actions { width: 100%; }
  .phead__actions .lss-btn--lg { padding: 10px 14px; font-size: 14px; }

  /* KPI grid collapses to single column at the smallest phone widths */
  .pkpis { grid-template-columns: repeat(2, 1fr); gap: 10px; }

  /* Location cards stack two-up on phone */
  .loc-grid { grid-template-columns: minmax(0, 1fr); } /* LOC_GRID_MINWIDTH_2026-08-06 */

  /* Notification panel becomes nearly full-width on phone */
  .npanel { width: 92vw; max-width: 360px; right: -8px; }

  /* Shift grid (instructor home) stacks */
  .shift-grid { grid-template-columns: 1fr; }

  /* Online courses + player */
  .online-grid { grid-template-columns: 1fr; }
  .player { grid-template-columns: 1fr; }
  .player__rail { position: static; }
  .player__main { padding: 20px 16px; }

  /* Modal padding tightens; drawers go nearly full-screen */
  .pmodal-scrim { padding: 12px; }
  .pmodal__head { padding: 14px 16px; }
  .pmodal__body { padding: 16px; }

  /* Footer buttons stack instead of overflowing. column-reverse puts the
     primary action (last in the DOM) at the top of the stack and anything
     destructive at the bottom, which is the safer order under a thumb. */
  .pmodal__actions { flex-direction: column-reverse; align-items: stretch; }
  .pmodal__actions .pmodal__actions-start { margin-right: 0; }
  /* .inline is `display:inline-flex` in components.css at equal specificity,
     so this is deliberately more specific rather than relying on load order. */
  .pmodal__actions > form,
  .pmodal__actions > form.inline { display: block; width: 100%; }
  /* border-box explicitly: this codebase has no global box-sizing reset, so
     width:100% on a padded button would overflow its own container. */
  .pmodal__actions .lss-btn { width: 100%; box-sizing: border-box; justify-content: center; }
  .pdrawer { width: 100vw; max-width: 100vw; }

  /* Calendar shrinks comfortably */
  .cal__bar { flex-wrap: wrap; gap: 10px; padding: 12px; }
  .cal__legend { margin-left: 0; width: 100%; }
  .cal__cell { min-height: 76px; padding: 4px 5px; }
  .cal__dow { padding: 6px 5px; font-size: 10px; }
  .cal__event { font-size: 10.5px; padding: 2px 5px; }
  .cal__event-loc { display: none; }  /* save space on phone */

  /* Tables — generic ptable rows on phone go a touch tighter */
  .ptable__row { padding: 11px 14px; gap: 10px; }
}

/* SMALL PHONE — <= 480px (extra-narrow tweaks) */
@media (max-width: 480px) {
  .pkpis { grid-template-columns: 1fr; }
  .ptb__crumb-role { display: none; }     /* breadcrumb only shows page */
  .ptb__crumb-sep { display: none; }
}

/* Sticky bottom UI safe-area handling — any element with .has-safe-bottom
   gets bottom padding equal to the device's bottom inset (home indicator). */
.has-safe-bottom { padding-bottom: max(16px, env(safe-area-inset-bottom)); }
.wr-banner { padding-bottom: max(10px, env(safe-area-inset-bottom)); }

/* Scrollable tab bars on mobile — tabs that overflow horizontally scroll
   instead of wrapping or being cut off. Used by Settings tab bar etc. */
@media (max-width: 820px) {
  .tabs {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    flex-wrap: nowrap;
    scrollbar-width: thin;
  }
  .tabs::-webkit-scrollbar { height: 4px; }
  .tabs::-webkit-scrollbar-thumb { background: var(--border-default); border-radius: 2px; }
  .tab { white-space: nowrap; flex: none; padding: 11px 14px; }
}

/* Reduced motion — collapse drawer + scrim animations site-wide.
   The existing @media (prefers-reduced-motion) above already collapses
   transitions; this rule belt-and-braces the new mobile-specific ones. */
@media (prefers-reduced-motion: reduce) {
  .psb, .psb-scrim, .ptb__hamburger { transition: none !important; }
}

/* iOS form-field zoom prevention — inputs / selects / textareas at 16px+
   (Safari only auto-zooms if focused field has font-size < 16px). */
@media (max-width: 820px) {
  input:not([type=checkbox]):not([type=radio]):not([type=hidden]),
  select, textarea,
  .lss-input input, .lss-select select {
    font-size: 16px;
  }
  /* Compact mini-select still needs 16px to avoid zoom but visually tighter */
  .mini-select { font-size: 16px; padding: 7px 26px 7px 10px; }
  /* Match the same on form-field labels inside lss-input wrappers */
  .lss-input { font-size: 16px; }
}

/* Touch-target safety sweep (44×44 minimum per WCAG 2.5.5).
   These rules apply at *all* viewport widths so admin desktop users
   still get bigger targets without disrupting layout. Padding-only
   changes — no width/height set that would break flex sizing. */
.npanel__mark { min-width: 44px; min-height: 44px; padding: 8px 12px; }
.wr-banner__close { min-width: 44px; min-height: 44px; padding: 4px 12px; }
.lss-alert__close { min-width: 44px; min-height: 44px; padding: 8px; }
.icon-btn { min-width: 44px; min-height: 44px; }
.cal__nav button { min-width: 44px; min-height: 44px; }
.pdrawer__hero .close { min-width: 44px; min-height: 44px; }
.pmodal__close { min-width: 44px; min-height: 44px; }
.att-allbtn { min-height: 44px; padding: 8px 12px; }

/* ============================================================
   Mobile card-mode for data tables (Pass 2)

   Apply .list-table-shell--card-mobile to any table-like wrapper to
   trigger the swap from a row-grid on desktop into a stacked
   single-column card layout on phones.

   Markup contract:
     <div class="ppanel list-table-shell--card-mobile">
       <div class="ptable__head"> ... </div>     (hidden on phone)
       <div class="card-row" data-card-title="...">
         <div class="card-row__field" data-label="Label">Value</div>
         ...
         <div class="card-row__actions"> ... </div>
       </div>
     </div>

   On desktop, the existing grid layout is preserved verbatim — the
   pattern only activates at <=768px.
   ============================================================ */

/* Roster attendee list (course-roster.php) — desktop 7-column grid; the
   card-mobile media query below stacks it into cards on phones. The grid
   lives in a class (not an inline style) so the mobile display:block can
   actually override it. */
.roster-row--head,
.roster-row {
  display: grid;
  /* ROSTER_NOTIFY_COL_2026-10-06 — 7 columns became 8: Notify sits between
     Station / Dept and Enrolled. 76px holds a checkbox over a short state word
     ("queued", "sent Oct 1"); Station / Dept gives up 10px of its minimum to
     pay for it, because it is the one track here that can ellipsis safely.

     Only course-roster.php uses this 8-track form. attendance.php and
     results.php also name .roster-row but always with .roster-row--pair, which
     redefines grid-template-columns and is declared after this rule, so they
     are unaffected — see ROSTER_ROW_PAIR_2026-09-13 below. */
  /* ROSTER_UNQUEUE_2026-10-06 — Notify widened 76px -> 96px for the longer
     "Notify local admin" heading, which wraps to two lines at 11px uppercase.
     The two flexible tracks (Name, Station / Dept) absorb it; the row's minimum
     goes from 912px to 932px, still clear of the 768px card-mobile breakpoint. */
  /* ROSTER_NOTIFY_INLINE_2026-10-06 — Notify 96px -> 132px. The header's
     tick now sits BESIDE "Notify local admin" rather than above it, so the
     track has to hold a 20px box plus the text. At 132px the label wraps
     once ("NOTIFY LOCAL" / "ADMIN") instead of three times, which is what
     was making this row twice the height of every other column heading.
     Row minimum goes 932px -> 968px; the card-mobile breakpoint is 768px. */
  grid-template-columns: minmax(160px, 2fr) 74px minmax(100px, 1.3fr) 132px 100px 140px 82px 96px;
  gap: 12px; align-items: center;
}
.roster-row--head { padding: 8px 14px; }
.roster-row { padding: 10px 14px; }

/* ROSTER_ROW_PAIR_2026-09-13. attendance.php and results.php borrow
   .roster-row for rows that hold TWO children, not seven. Without this the
   second child lands in the 74px count column above — and .att-toggle is
   overflow:hidden, so the Present / Late / Absent buttons were not squeezed,
   they were gone: 74px of box for 226px of buttons, on a full-width desktop.

   A modifier rather than a class of its own, so the phone block at 820px and
   the card-mobile shell selectors — both of which name .roster-row — go on
   applying with nothing to keep in sync. */
.roster-row--pair { grid-template-columns: minmax(0, 1fr) auto; }

@media (max-width: 768px) {
  /* !important (2026-07-17 UX_UI_AUDIT): head-row hide has to beat the
     `.list-table-shell--card-mobile .ptable__row { display:block }` rule
     below — same specificity ties on `.ptable__head.ptable__row` markup
     (users.php:722, dashboard.php, my-students.php, etc.). Without this,
     desktop column labels ("USER / EMAIL / ROLE / STATUS / ACTIONS")
     leak onto phones as a stacked orphan block above the first card.
     all-courses.php previously carried this fix inline; hoisted here so
     every list-table-shell--card-mobile consumer inherits. */
  .list-table-shell--card-mobile .ptable__head,
  .list-table-shell--card-mobile .doc-row--head,
  .list-table-shell--card-mobile .inv-row--head,
  .list-table-shell--card-mobile .roster-row--head,
  .list-table-shell--card-mobile .ml-row--head { display: none !important; }
  .list-table-shell--card-mobile .ml-row {
    grid-template-columns: none !important;
    display: block;
    padding: 14px;
    border-bottom: 1px solid var(--border-subtle);
  }
  .list-table-shell--card-mobile .ml-row > * {
    margin-bottom: 6px; white-space: normal !important;
    text-overflow: clip !important;
  }

  /* Card row outer (works for either .card-row or .ptable__row or .doc-row
     or .inv-row inside the shell). Resets the grid into stacked cards. */
  .list-table-shell--card-mobile .card-row,
  .list-table-shell--card-mobile .ptable__row,
  .list-table-shell--card-mobile .doc-row,
  .list-table-shell--card-mobile .inv-row,
  .list-table-shell--card-mobile .roster-row {
    display: block;
    padding: 14px 14px;
    border-bottom: 1px solid var(--border-subtle);
  }
  .list-table-shell--card-mobile .card-row:last-child,
  .list-table-shell--card-mobile .ptable__row:last-child,
  .list-table-shell--card-mobile .doc-row:last-child,
  .list-table-shell--card-mobile .inv-row:last-child,
  .list-table-shell--card-mobile .roster-row:last-child { border-bottom: 0; }

  /* Card header — primary identifier line (name, invoice #, etc.) */
  .card-row__header {
    display: flex; align-items: center; gap: 10px;
    margin-bottom: 10px;
  }
  .card-row__header b {
    font-size: 15px; color: var(--text-strong); display: block;
    line-height: 1.25;
  }
  .card-row__header span { font-size: 12.5px; color: var(--text-muted); }

  /* Card body — label-on-left, value-on-right key fields */
  .card-row__field {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: 12px; padding: 5px 0;
    font-size: 13.5px;
  }
  .card-row__field::before {
    content: attr(data-label);
    font-size: 11px; font-weight: 700; letter-spacing: .04em;
    text-transform: uppercase; color: var(--text-faint);
    flex: none;
  }
  .card-row__field > * { text-align: right; }
  .card-row__field--full {
    flex-direction: column; align-items: stretch; gap: 4px;
  }
  .card-row__field--full > * { text-align: left; }

  /* Card actions strip at the bottom */
  .card-row__actions {
    display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap;
    padding-top: 10px; margin-top: 10px;
    border-top: 1px solid var(--border-subtle);
  }
  .card-row__actions .lss-btn { flex: 1 1 auto; min-width: 0; }

  /* For tables built before the card pattern: if a row uses .ptable__row
     directly, we still need to surface its content readably. Provide a
     best-effort fallback: stack children with reasonable spacing and pull
     row-actions to the bottom as a footer strip. Pages that adopt the
     full .card-row__field pattern get richer label/value rows. */
  .list-table-shell--card-mobile .ptable__row,
  .list-table-shell--card-mobile .doc-row,
  .list-table-shell--card-mobile .inv-row {
    /* Override inline grid-template-columns: setting display:block (above)
       makes the inline grid-template-columns moot. */
    grid-template-columns: none !important;
  }
  .list-table-shell--card-mobile .ptable__row > *,
  .list-table-shell--card-mobile .doc-row > *,
  .list-table-shell--card-mobile .inv-row > * {
    margin-bottom: 6px;
  }
  .list-table-shell--card-mobile .ptable__row > *:last-child,
  .list-table-shell--card-mobile .doc-row > *:last-child,
  .list-table-shell--card-mobile .inv-row > *:last-child { margin-bottom: 0; }

  /* Pull row-actions / sel buttons into a footer strip */
  .list-table-shell--card-mobile .row-actions {
    justify-content: flex-end; flex-wrap: wrap;
    padding-top: 8px; margin-top: 6px;
    border-top: 1px solid var(--border-subtle);
  }
  .list-table-shell--card-mobile .row-actions .lss-btn,
  .list-table-shell--card-mobile .row-actions .icon-btn { flex: 0 0 auto; }

  /* Checkboxes for bulk-select: bigger tap target inside the card */
  .list-table-shell--card-mobile .checkbox-cell { padding: 4px 0; }
}

/* ============================================================
   Mobile overflow containment (MOBILE_SWEEP_2026-08-08)

   Found by rendering all 123 authenticated pages into a 390px-wide
   iframe on localhost and measuring documentElement.scrollWidth.
   28 pages scrolled sideways. Three root causes, fixed here once
   instead of 28 times:

     1. Long unbroken tokens. The dashboard error widget prints raw
        messages ("Failed opening required '/Applications/XAMPP/...'"),
        activity-log prints subjects, security-center prints user
        agents. A single 540px-wide "word" sets the min-content width
        of every grid/flex ancestor, so the whole page grows to fit
        it. overflow-wrap:anywhere is the only value that lowers
        min-content width -- break-word does not.

     2. Bare <table> elements with no scroll shell. There is no base
        .list-table-shell rule (only the --card-mobile modifier), so
        a six-column table just pushed the document wider. Worst
        offenders: migrations 762px, forms-data 738px, privacy-
        dashboard 623px against a 390px screen.

     3. Controls sized to their content -- a <select> is as wide as
        its longest <option>, which on notify-subscribers is a full
        course title.

   Everything here is scoped to :is(.pcontent, .pubmain) so the topbar, sidebar and
   drawers -- which have their own deliberate overflow rules -- are
   untouched.
   ============================================================ */
@media (max-width: 768px) {

  /* 1. Break mid-token rather than push the page wider. Declared on
        the container so it inherits down to the <b>/<span>/<code>
        leaf that actually carries the long string. Controls opt out:
        a button label broken mid-word reads as a rendering fault,
        and control labels are short enough not to be the problem. */
  :is(.pcontent, .pubmain) { overflow-wrap: anywhere; }
  :is(.pcontent, .pubmain) :is(.lss-btn, button, .tab, .lss-badge, .seg-opt,
                .icon-btn, .chip, .ptable__head) {
    overflow-wrap: normal;
  }
  /* Table cells opt out too. They do not need it -- rule 2 below turns
     every table into its own scroller -- and inside the table layout
     algorithm `anywhere` is actively harmful: it lets the algorithm
     squeeze a column until "Complete" renders as "Compl / ete". With
     `normal` the cell's min-content is its longest whole word, the table
     takes the width it needs, and the scroller absorbs the difference. */
  :is(.pcontent, .pubmain) table :is(th, td) { overflow-wrap: normal; }

  /* 2. Any table that has not opted out becomes its own horizontal
        scroller. display:block on <table> keeps the internal table
        layout (the row groups generate an anonymous table box) while
        giving us a block box we can scroll. Tables narrower than the
        column no longer stretch to 100% on phones -- an acceptable
        trade against the page itself scrolling sideways. */
  :is(.pcontent, .pubmain) table:not(.no-mobile-scroll) {
    display: block;
    max-width: 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }

  /* 3. Controls must never exceed their column. */
  :is(.pcontent, .pubmain) select,
  :is(.pcontent, .pubmain) input:not([type="checkbox"]):not([type="radio"]),
  :is(.pcontent, .pubmain) textarea { max-width: 100%; }
  :is(.pcontent, .pubmain) pre { overflow-x: auto; }

  /* Grid and flex children default to min-width:auto, which refuses
     to shrink below content. A panel is always safe to shrink. */
  :is(.pcontent, .pubmain) .ppanel { min-width: 0; }

  /* Action bars. A nowrap flex row of buttons is the most common remaining
     cause of a few pixels of sideways scroll -- privacy-dashboard's page
     header (412px of Edit Policy / Breach Log / View Public Policy in a
     347px column), error-log's bulk bar, wallet-settings' pane pair.
     Wrapping only engages when the row does not fit, so rows that already
     fit are pixel-identical. Inline style="display:flex" is matched by
     attribute because most of these toolbars carry no class.

     COLUMN flex containers are excluded on purpose. flex-wrap:wrap on a
     `flex-direction:column` box makes it wrap into a SECOND COLUMN once the
     items exceed its height, which widened account-privacy.php from 375px to
     499px -- the opposite of the intent. Caught by re-measuring after the
     rule went in; the exclusion is what keeps it fixed. */
  :is(.pcontent, .pubmain) [style*="display:flex"]:not([style*="flex-direction:column"]):not([style*="flex-direction: column"]):not([style*="flex-flow:column"]):not([style*="flex-flow: column"]),
  :is(.pcontent, .pubmain) [style*="display: flex"]:not([style*="flex-direction:column"]):not([style*="flex-direction: column"]):not([style*="flex-flow:column"]):not([style*="flex-flow: column"]),
  :is(.pcontent, .pubmain) :is(.phead, .el-bulk, .ws-grid, .row-actions) { flex-wrap: wrap; }

  /* Buttons keep whole words but may take a second line. wallet-settings'
     "Send Test Pass to andrew@lifesupportservices.ca" is a single 420px
     button -- no phone can show it on one line, and nowrap made it push the
     document instead of wrapping. */
  :is(.pcontent, .pubmain) .lss-btn { white-space: normal; }

  /* Utility: opt a fixed-column grid into a single stacked column on phones.
     For inline grid-template-columns that a media query cannot otherwise
     reach (public-services' "1fr 110px 110px 110px" pricing row). */
  .grid-stack-mobile { grid-template-columns: 1fr !important; }

  /* Topbar action cluster. Found by sweeping as a STAFF user: staff and
     public get a Sign Out form in .ptb__right instead of the account menu,
     and because .ptb__right is flex-shrink:1 with min-width:0 it shrank to
     102px while its own children still needed 129px -- so Sign Out sat 27px
     past the right edge of the phone on messages / account. The breadcrumb
     yields instead; it is the one thing in the bar that can be truncated
     without costing the user an action. */
  .ptb__right { flex: 0 0 auto; }
  .ptb__crumb { min-width: 0; overflow: hidden; }

  /* Card mode: desktop single-line truncation has to be released or
     the stacked card shows a one-line stub where the whole point of
     card mode is that the field gets the full width. */
  .list-table-shell--card-mobile .card-row__field,
  .list-table-shell--card-mobile .card-row__field * {
    white-space: normal !important;
    text-overflow: clip !important;
  }
}

/* ============================================================
   Mobile accordion fieldsets (Pass 3)

   Add .mobile-accordion to a <details><summary>…</summary>…</details>
   block. On desktop the block is always expanded (auto open=""). On phones
   the <summary> becomes a tap-to-expand card header.

   For pages that already use <fieldset>+<legend>, wrap each fieldset in
   <details class="mobile-accordion" open> with a <summary> mirroring the
   legend text. JS sets `open=""` on desktop so nothing is hidden there.
   ============================================================ */

.mobile-accordion {
  background: #fff; border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg, 12px); margin-bottom: 14px;
  overflow: hidden;
}
.mobile-accordion > summary {
  list-style: none; cursor: pointer;
  padding: 14px 16px; font-weight: 700; font-size: 15px;
  color: var(--text-strong);
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  min-height: 44px;
  background: var(--lss-ink-50, #f6f7f9);
  border-bottom: 1px solid var(--border-subtle);
}
.mobile-accordion > summary::-webkit-details-marker { display: none; }
.mobile-accordion > summary::after {
  content: ""; width: 12px; height: 12px;
  border-right: 2px solid var(--text-muted); border-bottom: 2px solid var(--text-muted);
  transform: rotate(45deg);
  transition: transform 0.18s var(--ease-standard, ease);
  flex: none;
}
.mobile-accordion[open] > summary::after { transform: rotate(-135deg); }
.mobile-accordion > .mobile-accordion__body { padding: 16px; }

/* Desktop — accordion behaves like a section: always open, summary smaller */
@media (min-width: 821px) {
  .mobile-accordion > summary {
    cursor: default;
    background: transparent;
    border-bottom: 1px solid var(--border-subtle);
    font-size: 16px;
  }
  .mobile-accordion > summary::after { display: none; }
}

/* ============================================================
   Lightweight collapsible — applied by JS to existing sections
   that mark themselves with [data-mobile-collapse]. The JS injects
   a button just above the section's first heading on phone, and
   toggles a .is-collapsed class which CSS uses to hide the body.

   Markup contract (no structural rewrite needed):
     <section class="ppanel cc-card" data-mobile-collapse data-mobile-title="Prerequisites">
       <header>...<h2>Prerequisites</h2>...</header>
       <body content...>
     </section>

   The JS wraps everything after the first <header>/<h2>/<h3> in a
   .mobile-collapse__body div on init, then toggles its display.
   ============================================================ */

@media (max-width: 820px) {
  [data-mobile-collapse] {
    position: relative;
    padding-top: 0 !important;  /* hide native top padding so toggle sits flush */
  }
  [data-mobile-collapse] > .mobile-collapse__toggle {
    display: flex; width: 100%; align-items: center; gap: 10px;
    padding: 14px 16px; min-height: 48px;
    background: var(--lss-ink-50, #f6f7f9);
    border: 0; border-bottom: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg, 12px) var(--radius-lg, 12px) 0 0;
    font-family: var(--font-sans); font-weight: 700; font-size: 15px;
    color: var(--text-strong); text-align: left; cursor: pointer;
    margin: 0 -18px;  /* visually fill horizontal padding of host card */
  }
  [data-mobile-collapse] > .mobile-collapse__toggle::after {
    content: ""; width: 11px; height: 11px; margin-left: auto;
    border-right: 2px solid var(--text-muted); border-bottom: 2px solid var(--text-muted);
    transform: rotate(45deg);
    transition: transform 0.18s var(--ease-standard, ease);
  }
  [data-mobile-collapse]:not(.is-collapsed) > .mobile-collapse__toggle::after {
    transform: rotate(-135deg);
  }
  [data-mobile-collapse] > .mobile-collapse__toggle:focus-visible {
    outline: 3px solid var(--lss-orange-500); outline-offset: -3px;
  }
  [data-mobile-collapse].is-collapsed > .mobile-collapse__body { display: none; }
  [data-mobile-collapse] > .mobile-collapse__body { padding-top: 14px; }
  /* Hide the inner duplicate heading once the toggle exposes it as the label. */
  [data-mobile-collapse] > .mobile-collapse__body > header > h1:first-child,
  [data-mobile-collapse] > .mobile-collapse__body > header > h2:first-child,
  [data-mobile-collapse] > .mobile-collapse__body > header > h3:first-child,
  [data-mobile-collapse] > .mobile-collapse__body > h1:first-child,
  [data-mobile-collapse] > .mobile-collapse__body > h2:first-child,
  [data-mobile-collapse] > .mobile-collapse__body > h3:first-child,
  [data-mobile-collapse] > .mobile-collapse__body > legend:first-child {
    display: none;
  }
  /* If the heading was wrapped in an inline-div header that had a side-action
     (e.g. a search input on the cc-prereqs card), let it stack on phone. */
  [data-mobile-collapse] > .mobile-collapse__body > header {
    display: block !important;
  }
  /* Also hide ppanel__head heading when present — the toggle replaces it. */
  [data-mobile-collapse] > .mobile-collapse__body > .ppanel__head {
    display: none;
  }

  /* When the marker is on a panel with no horizontal padding, undo the
     negative margin so the toggle doesn't bleed outside. */
  [data-mobile-collapse][data-mobile-collapse-flush] > .mobile-collapse__toggle {
    margin: 0;
    border-radius: var(--radius-lg, 12px) var(--radius-lg, 12px) 0 0;
  }
}
/* Desktop — toggle is invisible / inert; sections render as before. */
@media (min-width: 821px) {
  .mobile-collapse__toggle { display: none; }
}

/* ============================================================
   Big-button mobile workflows (Pass 4)
   ============================================================ */

/* Attendance — big thumb-friendly P/L/A buttons on phone */
@media (max-width: 820px) {
  .roster-row { display: block; padding: 14px; }
  .roster-row .person { margin-bottom: 12px; }
  .att-toggle { width: 100%; }
  .att-toggle label { flex: 1 1 0; }
  .att-toggle .at-opt {
    width: 100%; justify-content: center;
    padding: 14px 6px !important; font-size: 14px !important;
    min-height: 48px;
  }
  /* M5 (FOLLOWUPS_ROLLUP_2026-07-17), CORRECTED by
     FORM_VIEW_VALUE_LEFT_CLIP_2026-07-18: the original two-sided
     mask-gradient faded BOTH edges and clipped the first character
     of the leftmost attendance-session chip ("S" in "Session 1")
     even when the strip wasn't scrolled. A horizontally-scrolling
     strip only needs a fade on the side where content is hidden;
     when scrolled to the start there's nothing to the left, so
     right-edge-only is the safe default. Solid black from 0 to
     calc(100% - 24px), then fade out. */
  .att-sessions {
    gap: 6px; overflow-x: auto; padding-bottom: 4px; flex-wrap: nowrap;
    -webkit-mask-image: linear-gradient(to right,
      #000 0, #000 calc(100% - 24px), transparent 100%);
            mask-image: linear-gradient(to right,
      #000 0, #000 calc(100% - 24px), transparent 100%);
  }
  .att-session { min-width: 88px; flex: none; }
  .ppanel__head { flex-wrap: wrap; gap: 8px; padding: 12px 14px; }
}

/* Attendance filter chips (added in instructor pass) */
.att-filters {
  display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px;
}
.att-filter {
  padding: 8px 14px; border-radius: var(--radius-pill, 999px);
  border: 1px solid var(--border-default); background: #fff;
  font-weight: 600; font-size: 13px; color: var(--lss-ink-600);
  cursor: pointer; min-height: 36px;
}
.att-filter.is-on {
  background: var(--lss-navy-700); border-color: var(--lss-navy-700); color: #fff;
}

/* Results — pass/fail thumb-friendly toggle on phone (matches att-toggle) */
@media (max-width: 820px) {
  .result-toggle { width: 100%; }
  .result-toggle label { flex: 1 1 0; display: inline-flex; }
  .result-toggle .rt-opt {
    width: 100%; justify-content: center;
    padding: 14px 6px !important; font-size: 14px !important;
    min-height: 48px;
  }
}

/* Hours — sticky clock-in/out bar on phone */
.clock-sticky {
  position: sticky; bottom: 0;
  background: #fff;
  border-top: 1px solid var(--border-subtle);
  padding: 12px 14px;
  padding-bottom: max(12px, env(safe-area-inset-bottom));
  display: flex; gap: 10px; align-items: center;
  z-index: 25;
  margin: 0 -14px;
}
@media (min-width: 821px) {
  .clock-sticky {
    position: static; padding: 0; margin: 0;
    border-top: 0; background: transparent;
  }
}
.clock-sticky form { flex: 1; }
.clock-sticky .lss-btn {
  width: 100%; min-height: 52px; font-size: 16px;
}

/* ---------- Mobile tap-target lift ----------
   Apple HIG / Material both call for ≥44px tap targets. Several inputs and
   filter chips were undersized for thumb-on-glass use; bump them up on
   narrow viewports only so desktop density is preserved. */
@media (max-width: 820px) {
  .hours-input, .clock-input {
    min-height: 44px; font-size: 16px; padding: 8px 10px;
    width: auto; min-width: 80px;
  }
  .att-filter, .ch-filter a, .pkpi {
    min-height: 44px;
  }
  /* Calendar segmented control */
  .cal__toggle button, .cal__toggle a {
    min-height: 44px; padding: 10px 16px;
  }
  /* Bottom-shadow nav touch padding to ensure 44px even at the safe-area bottom */
  .psb__item { min-height: 44px; }
}

/* ============================================================
 *  Command palette (Cmd+K) — 2026-06-27
 *
 *  Centered modal overlay. The scrim sits below it; both are
 *  rendered server-side and toggled via the `hidden` attribute
 *  (no flash on first paint).
 *
 *  Keyboard accessibility:
 *  - Esc closes (handled in portal.js).
 *  - Arrow keys move .is-selected (rendered as orange row).
 *  - Enter activates the .is-selected row.
 *  - Tab returns focus to the next focusable in the page —
 *    palette is short-lived and re-shows on every Cmd+K.
 *
 *  Mobile: full-screen drawer instead of centered card.
 * ============================================================ */
/* 2026-07-17 UX_UI_AUDIT: bumped from 200/201 to 210/211 so the global Cmd-K
   palette always beats the form-builder field drawer (.fb-drawer, z-200) if
   an admin opens the palette while editing a form. */
.cmdk-scrim {
  position: fixed; inset: 0; background: rgba(20,22,40,0.45);
  z-index: 210; -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
  animation: cmdkFadeIn 120ms ease-out;
}
/* The HTML `hidden` attribute must win over our `display:flex` below.
   Without this, setting `palette.hidden = true` in JS leaves the panel
   visible because the more-specific class rule wins over [hidden]. */
.cmdk[hidden], .cmdk-scrim[hidden] { display: none !important; }

/* HIDDEN_MEANS_HIDDEN_2026-09-12 ------------------------------------------
   An author rule that sets `display` beats the browser's own
   `[hidden] { display: none }` — author origin wins over user-agent origin
   regardless of specificity. So any class that sets display quietly makes the
   hidden attribute decorative on every element that carries it, and the
   JavaScript setting el.hidden = true does nothing at all.

   This codebase had found that out eight separate times and patched it eight
   separate ways: .cmdk, .cmdk-scrim, .thread-drawer-scrim, .npanel,
   .pmega__panel here, .fb-pane, .ft-modal, .lss-form__section-wrap,
   .lss-form__jumpchip elsewhere. Each fix named the trap and then fixed one
   class. The ninth was .pub-hint on the public booking form, which showed
   "We found an account with this email" to every visitor from first paint,
   on an empty form, and had survived an earlier audit that blamed autofill
   and rewrote the JavaScript — the JavaScript was never the problem.

   Also .lss-btn { display: inline-flex }, which is used with hidden in seven
   files (settings.php, my-account.php, checkin.php, store.php, media.php,
   mail-settings.php, course-equipment.php), so those buttons were all
   visible before anything asked for them.

   One rule instead. Verified safe first: 96 places reveal an element by
   removing the attribute, and nothing anywhere shows a still-hidden element
   by assigning style.display, which is the only thing !important could
   strand. The per-class guards above are now redundant and harmless; they
   stay as a record of where this bit. */
[hidden] { display: none !important; }

.cmdk {
  position: fixed; z-index: 211;
  left: 50%; top: 12vh; transform: translateX(-50%);
  width: min(620px, calc(100vw - 32px));
  max-height: 70vh;
  background: #fff; color: var(--lss-ink-800, #22232c);
  border-radius: 14px;
  box-shadow: 0 24px 60px rgba(20,22,40,0.30);
  display: flex; flex-direction: column;
  overflow: hidden;
  animation: cmdkSlideIn 140ms ease-out;
}
.cmdk__head {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 18px;
  border-bottom: 1px solid var(--lss-ink-200, #e1e3e8);
}
.cmdk__head-ic { color: var(--lss-navy-700, #232160); display: inline-flex; }
.cmdk__head-ic svg { width: 18px; height: 18px; }
.cmdk__input {
  flex: 1; border: 0; outline: 0;
  background: transparent; font: 600 16px var(--font-sans);
  color: var(--lss-ink-800, #22232c);
  padding: 4px 0;
}
.cmdk__input::placeholder { color: var(--lss-ink-400, #8c8f99); font-weight: 500; }
.cmdk__hint {
  background: var(--lss-ink-100, #eef0f3); color: var(--lss-ink-600, #4c4f5a);
  font: 600 11px var(--font-sans); padding: 3px 8px; border-radius: 5px;
}
.cmdk__list {
  list-style: none; margin: 0; padding: 6px 0;
  overflow-y: auto; max-height: 50vh;
}
.cmdk__row {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 18px; cursor: pointer;
  border-left: 3px solid transparent;
}
.cmdk__row:hover { background: var(--lss-navy-50, #f3f3fa); }
.cmdk__row.is-selected {
  background: var(--lss-navy-50, #f3f3fa);
  border-left-color: var(--lss-orange-500, #ef6a2e);
}
.cmdk__row-ic { color: var(--lss-navy-700, #232160); display: inline-flex; flex: none; }
.cmdk__row-ic svg { width: 16px; height: 16px; opacity: .85; }
.cmdk__row-label {
  flex: 1; font-size: 14px; font-weight: 600;
  color: var(--lss-ink-800, #22232c);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cmdk__row-sec {
  font-size: 11px; color: var(--lss-ink-500, #6b6e7a);
  text-transform: uppercase; letter-spacing: .08em; flex: none;
}
.cmdk__empty {
  padding: 22px 18px; text-align: center;
  color: var(--lss-ink-500, #6b6e7a); font-size: 14px;
}
.cmdk__foot {
  display: flex; gap: 18px;
  padding: 10px 18px; background: var(--lss-ink-50, #f7f8fa);
  border-top: 1px solid var(--lss-ink-200, #e1e3e8);
  font-size: 12px; color: var(--lss-ink-500, #6b6e7a);
}
.cmdk__foot kbd {
  background: #fff; border: 1px solid var(--lss-ink-200, #e1e3e8);
  padding: 2px 6px; border-radius: 4px; margin-right: 3px;
  font-family: var(--font-sans); font-weight: 600;
}
body.cmdk-open { overflow: hidden; }

@keyframes cmdkFadeIn  { from { opacity: 0; } to { opacity: 1; } }
@keyframes cmdkSlideIn { from { opacity: 0; transform: translate(-50%, -8px); }
                          to   { opacity: 1; transform: translate(-50%, 0); } }

/* Mobile: palette becomes a full-screen drawer */
@media (max-width: 600px) {
  .cmdk {
    top: 0; left: 0; transform: none; width: 100vw; height: 100vh;
    max-height: 100vh; border-radius: 0;
  }
  .cmdk__list { max-height: none; flex: 1; }
}

/* Breadcrumb: section label between role and page. */
.ptb__crumb-sec {
  color: var(--lss-navy-600, #322d77);
  font-weight: 600;
}

/* Mobile drawer: keep the divider clearly visible at small widths and
   ensure the search trigger has thumb-sized touch target. */
@media (max-width: 820px) {
  .psb__search { padding: 12px 12px; min-height: 44px; }
  .psb__divider { margin: 12px 14px 4px; }
  /* Mobile drawer accordion: open one section at a time */
  .psb.is-open .psb__group[open] ~ .psb__group[open] {
    /* JS handles the actual close; this is a safety belt so the drawer
       never grows taller than the viewport when two sections accidentally
       both stay open during a transition. */
  }
}

/* ============================================================
 *  Messaging — thread view (2026-06-28)
 *
 *  Desktop: two-column layout (thread + reply on the left, a
 *  participants aside on the right).
 *
 *  Mobile (<=768px):
 *    - Columns stack: thread fills the viewport, participants
 *      aside becomes an off-canvas bottom-sheet drawer reached
 *      via the "Participants (N)" button rendered in markup.
 *    - Reply composer becomes sticky to the bottom of the
 *      viewport (above the safe-area inset).
 *    - Textarea is 16px so iOS doesn't zoom on focus, and a
 *      small JS auto-grow keeps it sized to the typed content.
 *    - Message bubbles get more breathing room (~85% max-width)
 *      and timestamps render with the date stripped (time only)
 *      so they stay single-line.
 *
 *  Markup contract:
 *    <div class="thread-page" data-thread-id="…">
 *      <div class="thread-page__main">
 *        <div class="ppanel"><div class="ppanel__body" id="thread-body">
 *          <div id="msg-stream">…</div>
 *          <form class="thread-reply">…</form>
 *        </div></div>
 *      </div>
 *      <button class="thread-participants-toggle" hidden>Participants (N)</button>
 *      <aside class="thread-participants" id="thread-participants">…</aside>
 *      <div class="thread-drawer-scrim" hidden></div>
 *    </div>
 * ============================================================ */

.thread-page {
  display: grid;
  grid-template-columns: 1fr 280px;
  gap: 22px;
  align-items: start;
}
.thread-page__main { min-width: 0; }
.thread-participants-toggle { display: none; }

/* Message bubbles ------------------------------------------------ */
.msg {
  padding: 10px 12px;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background: #fff;
  max-width: 78%;
  word-wrap: break-word;
  overflow-wrap: anywhere;
}
.msg.is-self {
  margin-left: auto;
  background: #f1f4ff;
  border-color: #c8d3f2;
}
.msg.is-system {
  align-self: center;
  max-width: none;
  background: transparent;
  border: 0;
  color: var(--text-muted);
  font-size: 12.5px;
  padding: 4px 0;
}
.msg.is-system em { font-style: normal; }
.msg.is-system small { margin-left: 8px; color: var(--text-faint); }
.msg__head {
  display: flex; align-items: center; gap: 8px;
  font-size: 12.5px; color: var(--text-muted);
  margin-bottom: 4px;
}
.msg__head b { color: var(--text-strong); }
.msg__head small {
  margin-left: auto;
  font-size: 11.5px;
  color: var(--text-faint);
  white-space: nowrap;
}
.msg__avatar {
  flex: 0 0 28px; height: 28px;
  border-radius: 50%;
  background: #e1e4ee; color: var(--text-strong);
  display: inline-flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 12px;
}
.msg__body { font-size: 14px; color: var(--text-strong); line-height: 1.5; word-wrap: break-word; }

/* Reply composer ------------------------------------------------- */
.thread-reply {
  margin-top: 18px;
  border-top: 1px solid var(--border-subtle);
  padding-top: 14px;
}
.thread-reply__row {
  margin-top: 10px;
  display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
}
.thread-reply__urgent { display: flex; flex-direction: column; gap: 4px; }
.thread-reply__urgent small {
  font-size: 11.5px; color: var(--text-muted);
  margin-left: 30px; line-height: 1.35;
}

/* Mobile bottom-sheet drawer scrim (shared markup hook) */
.thread-drawer-scrim {
  position: fixed; inset: 0;
  background: rgba(20,22,40,0.45);
  z-index: 60;
  animation: threadDrawerFade 140ms ease-out;
}
.thread-drawer-scrim[hidden] { display: none; }
@keyframes threadDrawerFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes threadDrawerSlide {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}

/* Mobile: stack + drawer + sticky composer (<=768px) ------------ */
@media (max-width: 768px) {
  .thread-page {
    display: block;
    /* Reserve some scroll headroom so the sticky composer doesn't
       cover the last message when the user scrolls to the bottom. */
    padding-bottom: 0;
  }

  /* "Participants (N)" button at the top, full-width, big tap target. */
  .thread-participants-toggle {
    display: flex; align-items: center; justify-content: space-between;
    width: 100%;
    margin: 0 0 14px;
    padding: 12px 14px; min-height: 44px;
    background: #fff;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    font-family: var(--font-sans);
    font-weight: 600; font-size: 14px;
    color: var(--text-strong);
    cursor: pointer;
  }
  .thread-participants-toggle::after {
    content: ""; width: 9px; height: 9px;
    border-right: 2px solid var(--text-muted);
    border-bottom: 2px solid var(--text-muted);
    transform: rotate(45deg);
    margin-left: 8px;
  }
  .thread-participants-toggle .thread-participants-toggle__count {
    margin-left: 8px;
    color: var(--text-muted); font-weight: 500;
  }

  /* Hide the inline participants aside; reveal it as a slide-up
     bottom sheet when .is-open is toggled by JS. */
  .thread-participants {
    position: fixed; left: 0; right: 0; bottom: 0;
    z-index: 65;
    max-height: 78vh;
    overflow-y: auto;
    background: #fff;
    border-radius: 14px 14px 0 0;
    box-shadow: 0 -16px 40px rgba(20,22,40,0.20);
    transform: translateY(100%);
    transition: transform 0.24s var(--ease-out, cubic-bezier(.2,.7,.3,1));
    padding-bottom: max(16px, env(safe-area-inset-bottom));
  }
  .thread-participants.is-open {
    transform: translateY(0);
    animation: threadDrawerSlide 220ms var(--ease-out, cubic-bezier(.2,.7,.3,1));
  }
  /* Grab handle bar for the bottom sheet */
  .thread-participants::before {
    content: ""; display: block;
    width: 44px; height: 4px;
    margin: 8px auto 4px;
    border-radius: 999px;
    background: var(--border-default, #d6d8e0);
  }

  /* Bubbles breathe — 85% of viewport, larger body text */
  .msg { max-width: 85%; padding: 10px 12px; }
  .msg__body { font-size: 14.5px; }
  /* Timestamps stay single-line; the data-time-short attribute is set
     by JS on the inline <small> for compact rendering on phone. */
  .msg__head small {
    font-size: 11.5px; color: var(--text-faint);
    white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis;
    max-width: 9ch;
  }

  /* Sticky reply composer ------------------------------------- */
  .thread-reply {
    position: sticky; bottom: 0;
    z-index: 25;
    margin-top: 18px;
    /* Bleed into the .pcontent gutters so the composer is full-bleed */
    margin-left: -14px; margin-right: -14px;
    padding: 12px 14px;
    padding-bottom: max(12px, env(safe-area-inset-bottom));
    background: #fff;
    border-top: 1px solid var(--border-subtle);
  }
  .thread-reply textarea.lss-textarea {
    width: 100%;
    min-height: 80px;
    /* 16px prevents iOS Safari from zooming on focus. */
    font-size: 16px;
    line-height: 1.45;
    resize: none;        /* JS handles auto-grow */
  }
  .thread-reply__urgent .lss-check { align-items: flex-start; }
  .thread-reply__urgent .lss-check__label {
    font-size: 14px; line-height: 1.35;
  }
  .thread-reply__urgent small {
    margin-left: 30px; font-size: 12px; color: var(--text-muted);
    display: block;
  }

  /* Inbox row — let the title wrap onto two lines so subjects don't
     get truncated next to the timestamp on narrow viewports. */
  .mlist__title { flex-wrap: wrap; align-items: flex-start; }
  .mlist__title > span:first-child { flex: 1 1 100%; min-width: 0; }
  .mlist__title small { flex: 1 1 100%; margin-top: 2px; }
  .mlist__row { padding: 14px; gap: 12px; }

  /* Compose page — keep the recipient pill picker scrollable but
     full-bleed, and bump form font-size to 16px so iOS Safari
     doesn't zoom on focus. */
  .cmpbox { padding: 14px; border-radius: var(--radius-md); }
  .cmpbox .lss-input input,
  .cmpbox .lss-textarea,
  .cmpbox select { font-size: 16px; }
  .cmpbox .recipients { max-height: 220px; }
  .cmpbox .recipients label { padding: 7px 12px; min-height: 34px; }
}

/* SEND_BTN_BREAKPOINT_2026-08-05 — the jumbo full-width Send (button above
   the Urgent toggle, 48px tall) used to switch on at 768px, which caught
   half-screen DESKTOP windows: a local admin with a ~720px window got the
   thumb-sized phone button ("the send button is really wide on desktop").
   The sticky composer + 16px textarea (iOS zoom guard) stay at 768 — they
   are harmless in a narrow desktop window — but the touch-first button
   treatment now waits for genuinely phone-sized widths. */
@media (max-width: 560px) {
  .thread-reply__row {
    margin-top: 10px;
    display: flex; flex-direction: column; align-items: stretch;
    gap: 10px;
  }
  .thread-reply__row .lss-btn[type="submit"] {
    order: -1;            /* Send button above the Urgent toggle */
    width: 100%;
    min-height: 48px;
    font-size: 15px;
  }
  .thread-reply__urgent { width: 100%; }
}


/* ============================================================
 * 2026-06-30 — Field-box scaling sweep.
 *
 * Kills the recurring "narrow-column input overflow" pattern that
 * kept surfacing across course-create, course-edit, settings,
 * public-user, my-account, form-edit, and every other form that
 * uses inline `display:grid;grid-template-columns:1fr 1fr|repeat(N,minmax(0,1fr))`.
 *
 * Inline styles win over class-based media queries on specificity,
 * so we target them by attribute selector and force single-column
 * on phone. The 7-col day-picker uses `repeat(7,1fr)` which does
 * NOT match `1fr 1fr` or `minmax(0,1fr)`, so it stays 7-wide.
 *
 * Also caps intrinsic width on iOS-Safari date/time/datetime-local
 * inputs which otherwise ignore the container width and blow out
 * their 2-col cell.
 *
 * Sibling doc: LSS_MOBILE_FIELD_SCALING_2026-06-30.md
 * ============================================================ */
@media (max-width: 520px) {
  /* --- Inline multi-column grids collapse to single column --- */
  /* Matches: grid-template-columns:1fr 1fr, 1fr 1fr 1fr, 2fr 1fr, 1.4fr 1fr 1fr */
  form [style*="1fr 1fr"],
  .ppanel__body [style*="1fr 1fr"],
  form [style*="2fr 1fr"],
  .ppanel__body [style*="2fr 1fr"],
  form [style*="1.4fr 1fr"],
  .ppanel__body [style*="1.4fr 1fr"] {
    grid-template-columns: 1fr !important;
  }
  /* Matches: repeat(2,minmax(0,1fr)), repeat(3,minmax(0,1fr)),
     repeat(4,minmax(0,1fr)) — but NOT repeat(7,1fr) (day-picker). */
  form [style*="minmax(0,1fr)"],
  .ppanel__body [style*="minmax(0,1fr)"],
  form [style*="minmax(0, 1fr)"],
  .ppanel__body [style*="minmax(0, 1fr)"] {
    grid-template-columns: 1fr !important;
  }
  /* Fixed-width flex children (e.g. `flex:0 0 140px`) can't shrink and
     force a horizontal scroll. Let them shrink and wrap. */
  form [style*="flex:0 0"],
  form [style*="flex: 0 0"],
  .ppanel__body [style*="flex:0 0"],
  .ppanel__body [style*="flex: 0 0"] {
    flex: 1 1 100% !important;
    min-width: 0 !important;
  }

  /* --- Input intrinsic-width safety net --- */
  /* iOS Safari gives date/datetime-local/time inputs an intrinsic
     width larger than the container. Cap them so a 2-col row
     (before it collapses via the rules above) still fits. */
  .lss-input input[type="date"],
  .lss-input input[type="datetime-local"],
  .lss-input input[type="time"],
  input[type="date"],
  input[type="datetime-local"],
  input[type="time"] {
    max-width: 100% !important;
    min-width: 0 !important;
    width: 100%;
    box-sizing: border-box;
  }
  /* Wrappers must permit their children to shrink inside grid/flex parents. */
  .lss-input, .lss-select, .lss-textarea {
    min-width: 0 !important;
    max-width: 100% !important;
  }
  .lss-input input, .lss-select select, .lss-textarea {
    max-width: 100% !important;
    min-width: 0 !important;
    box-sizing: border-box;
  }
  /* Any inline-styled form control (e.g. `style="max-width:120px"`
     on the Instructors-required counter) must still shrink cleanly. */
  form input, form select, form textarea {
    min-width: 0;
    box-sizing: border-box;
  }

  /* --- Label hint suffixes --- */
  /* Intent was to keep "(optional)", "(syncs to X)", "(encrypted at rest)"
     from pushing a label onto two lines.

     LABEL_SPAN_NOWRAP_2026-07-27: but the selector catches EVERY span inside a
     field label, including long explanatory hints — and on the Course types
     edit modal one of those contains a "Certificates awaiting number" link.
     nowrap meant it could not break, so it ran to 1489px inside a 304px label
     and dragged the dialog's scroll width to 1533px against a 336px viewport.
     The modal scrolled sideways and read as "really wide" on a phone.

     A mobile-only rule breaking mobile layout is not a trade worth keeping: a
     two-line label is untidy, a dialog you have to pan sideways is unusable.
     Wrapping restored, with overflow-wrap so an unbreakable token (a long URL
     in a hint) can't reproduce the same overflow by another route.

     If a specific suffix genuinely must stay on one line, give it its own
     class and scope nowrap to that — not to every span in every label. */
  .field-label span,
  label.field-label span {
    white-space: normal;
    overflow-wrap: anywhere;
  }
}

/* ============================================================
 * 2026-06-30 — Notifications dropdown positioning on mobile.
 *
 * Previous rule (`right:-8px; width:92vw`) anchored the panel to
 * the bell button and let its right edge spill past the viewport,
 * which visually pushed the panel tight to the left with an
 * asymmetric right overflow.
 *
 * Switch to fixed positioning below the topbar with symmetric
 * 8px margins from the viewport edges — respects iOS safe area
 * and stays inside the visible width regardless of bell position.
 * ============================================================ */
@media (max-width: 520px) {
  .nbell { position: static; }
  .npanel {
    position: fixed;
    top: calc(56px + env(safe-area-inset-top, 0px) + 6px);
    left: max(8px, env(safe-area-inset-left, 0px));
    right: max(8px, env(safe-area-inset-right, 0px));
    width: auto;
    max-width: calc(100vw - 16px);
    max-height: calc(100vh - 56px - env(safe-area-inset-top, 0px) - 20px);
    display: flex;
    flex-direction: column;
  }
  /* Restore the [hidden] contract that `display: flex` above stomps on.
     The HTML5 `hidden` attribute is the JS-side toggle for the panel; the
     `display: flex` rule above raised specificity past the UA default
     `[hidden] { display: none }`, so on Safari mobile the panel stayed
     visible on load and after tapping the close X — the JS was correctly
     flipping the attribute but CSS refused to hide it. Bumping specificity
     to `.npanel[hidden]` puts the contract back. */
  .npanel[hidden] { display: none; }
  .npanel__list {
    max-height: none;
    flex: 1 1 auto;
    overflow-y: auto;
  }
  /* Bump the close-X to a proper touch target on mobile — WCAG 2.5.5
     baseline is 44×44px. Also nudge the icon slightly larger for
     one-handed thumb use. */
  .npanel__close { padding: 10px; min-width: 44px; min-height: 44px; }
  .npanel__close svg { width: 22px; height: 22px; }
}

/* M4 (FOLLOWUPS_ROLLUP_2026-07-17): mid-width band 521–819px.
   Below 520px the panel goes fixed + viewport-margined (see block
   above). At >=820px the desktop shape is fine. In between, the
   default absolute-positioned 360px panel anchored to .nbell can
   tuck tight against the right viewport edge and overflow the
   safe area. Clamp width so the panel always keeps 12px of gutter
   on each side and never exceeds its own 360px design width. */
@media (min-width: 521px) and (max-width: 819px) {
  .npanel {
    width: min(360px, calc(100vw - 24px));
    max-width: none; /* replaces the 86vw floor which becomes tight at ~600px */
    right: max(0px, env(safe-area-inset-right, 0px));
  }
}


/* ================================================================
   Icon-rail (compact sidebar) mode — nav compaction 2026-07-02
   body.psb-rail is toggled by portal.js (#psb-rail-toggle button);
   preference persists in localStorage. Desktop only: on the phone
   breakpoint the sidebar is already an off-canvas drawer, so the
   toggle is hidden and the class is never applied by JS.
   ================================================================ */
.psb__rail-toggle {
  display: flex; align-items: center; gap: 11px; padding: 9px 12px;
  border-radius: var(--radius-md); font-size: 13px; font-weight: 600;
  color: #8a88ad; cursor: pointer; border: 0; background: none;
  font-family: var(--font-sans); width: 100%; text-align: left;
  transition: background .12s, color .12s;
}
.psb__rail-toggle:hover { background: rgba(255,255,255,.06); color: #fff; }
.psb__rail-toggle:focus-visible { outline: 2px solid var(--lss-orange-500); outline-offset: 2px; }
.psb__rail-toggle-glyph { font-size: 12px; letter-spacing: -2px; transition: transform .2s ease; }
.psb__rail-toggle[aria-pressed="true"] .psb__rail-toggle-glyph { transform: rotate(180deg); }

@media (max-width: 820px) {
  .psb__rail-toggle { display: none; }
}

@media (min-width: 821px) {
  body.psb-rail .papp { grid-template-columns: 64px 1fr; }
  body.psb-rail .psb  { padding: 16px 8px; overflow-x: hidden; }

  /* Text goes away; icons remain. Section headers (summaries) hide
     entirely — every group is forced open by JS in rail mode — and a
     subtle rule marks each section boundary instead. */
  body.psb-rail .psb__label,
  body.psb-rail .psb__sec,
  body.psb-rail .psb__subhead,
  body.psb-rail .psb__loc,
  body.psb-rail .psb__logo,
  body.psb-rail .psb__search-label,
  body.psb-rail .psb__search-kbd,
  body.psb-rail .psb__pin-pill,
  body.psb-rail .psb__user div { display: none; }

  /* The text-wordmark logo variant carries an inline display style
     (logo_html emits it), so this one needs !important to win. */
  body.psb-rail .psb__logo { display: none !important; }
  body.psb-rail .psb__rail-logo {
    display: block; width: 40px; height: 40px; margin: 0 auto 8px;
    border-radius: 8px; object-fit: contain;
  }

  body.psb-rail .psb__group {
    border-top: 1px solid rgba(255,255,255,.08);
    padding-top: 4px; margin-top: 4px;
  }
  body.psb-rail .psb__group--pinned { border-top: 0; margin-top: 0; }

  body.psb-rail .psb__item {
    position: relative; justify-content: center; gap: 0; padding: 10px 0;
  }
  body.psb-rail .psb__search { justify-content: center; padding: 10px 0; }
  body.psb-rail .psb__user   { justify-content: center; padding: 8px 0; }
  body.psb-rail .psb__rail-toggle { justify-content: center; padding: 10px 0; }

  /* Badges compress to an unread-dot pinned to the icon's corner. */
  body.psb-rail .psb__badge {
    position: absolute; top: 4px; right: 12px;
    width: 8px; height: 8px; min-width: 0; padding: 0;
    font-size: 0; line-height: 0; border-radius: 50%;
    background: var(--lss-orange-500);
  }
}

/* Rail-mode square logo — hidden until body.psb-rail applies. */
.psb__rail-logo { display: none; }

/* ================================================================
   Legacy headed-card normalization — 2026-07-03
   A number of cards (dashboard widgets, forms report, migrations,
   my-certifications, form-submission) pad the .ppanel itself
   (style="padding:14px 18px") AND nest a .ppanel__head inside.
   The head then double-pads (14+16 / 18+20) and its divider floats
   inset from the card edges. Normalize every such card in one place:
   strip the head's class padding down and stretch its divider to the
   full card width with negative margins. Cards using the standard
   head+body anatomy are untouched.
   ================================================================ */
.ppanel[style*="padding:14px 18px"] > .ppanel__head {
  padding: 0 18px 12px;
  margin: 0 -18px 12px;
}

/* ================================================================
   Admin secondary nav — 2026-07-08
   Sticky chip strip that sits directly below the main topbar for
   role=admin, hosting the top-level control-panel surfaces that
   were previously part of the hamburger. Renders only when the
   .ptbsec element is emitted (server-side gated to admin), so it
   costs zero on non-admin pages. Horizontally scrollable on phone
   with edge fade masks; the current-page chip is highlighted and
   auto-scrolled into view by portal.js on load.
   z-index sits under the topbar's stacking context (topbar = 30)
   so open bell / role-switcher menus paint over it.
   ================================================================ */
.ptbsec {
  position: sticky;
  top: 60px; /* topbar height desktop */
  z-index: 29;
  background: #fff;
  border-bottom: 1px solid var(--border-subtle);
  display: block;
  overflow: hidden; /* fade masks bleed edge; scroll happens in inner */
}
.ptbsec__scroll {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 20px;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-behavior: smooth;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.ptbsec__scroll::-webkit-scrollbar { display: none; }
.ptbsec__grouplbl {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-faint, #7a809b);
  padding: 0 4px 0 6px;
  white-space: nowrap;
  flex: none;
}
.ptbsec__sep {
  width: 1px;
  height: 20px;
  background: var(--border-subtle, #e7e9ef);
  margin: 0 4px;
  flex: none;
}
.ptbsec__chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border-radius: 999px;
  background: var(--lss-ink-50, #f5f6fa);
  border: 1px solid var(--border-subtle, #e7e9ef);
  color: var(--lss-navy-700, #17203e);
  font-size: 13px;
  font-weight: 600;
  line-height: 1.2;
  text-decoration: none;
  white-space: nowrap;
  scroll-snap-align: center;
  flex: none;
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}
.ptbsec__chip:hover,
.ptbsec__chip:focus-visible {
  background: #fff;
  border-color: var(--lss-navy-300, #b6bed4);
  color: var(--lss-navy-800, #0e1a34);
  outline: none;
}
.ptbsec__chip:focus-visible {
  box-shadow: 0 0 0 2px var(--lss-navy-300, #b6bed4);
}
.ptbsec__chip.is-active {
  background: var(--lss-navy-700, #17203e);
  border-color: var(--lss-navy-700, #17203e);
  color: #fff;
}
.ptbsec__chip-ic {
  display: inline-flex;
  width: 14px; height: 14px;
  align-items: center; justify-content: center;
  opacity: .85;
}
.ptbsec__chip-ic svg { width: 14px; height: 14px; }
.ptbsec__chip.is-active .ptbsec__chip-ic { opacity: 1; }
.ptbsec__chip-badge {
  display: inline-flex;
  min-width: 18px;
  padding: 0 5px;
  height: 18px;
  align-items: center;
  justify-content: center;
  background: var(--lss-emergency, #c43d30);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  border-radius: 10px;
  margin-left: 2px;
}
/* Edge fade masks — hint at horizontal overflow on phones. Positioned
   absolutely so they don't push the chip strip; pointer-events off so
   taps pass through. */
.ptbsec__fade {
  position: absolute;
  top: 0; bottom: 0;
  width: 22px;
  pointer-events: none;
  z-index: 2;
}
.ptbsec__fade--left  { left: 0;  background: linear-gradient(to right, #fff, rgba(255,255,255,0)); }
.ptbsec__fade--right { right: 0; background: linear-gradient(to left,  #fff, rgba(255,255,255,0)); }
.ptbsec { position: sticky; } /* re-assert after absolutely-positioned children */
.ptbsec::before { content: ""; display: block; height: 0; } /* keep as block ctx */
.ptbsec { position: sticky; } /* no-op safety */

/* Body-class hooks driven by portal.js for smarter fade visibility.
   When the inner scroll is at the very left/right, the corresponding
   fade goes away so it doesn't imply more content than there is. */
.ptbsec.is-at-start .ptbsec__fade--left  { opacity: 0; }
.ptbsec.is-at-end   .ptbsec__fade--right { opacity: 0; }
.ptbsec__fade { transition: opacity .18s ease; }

/* Scroll arrow buttons — a mouse-friendly affordance for reaching chips
   that overflow the strip. Hidden by default; portal.js adds .has-overflow
   to .ptbsec only when the inner scroll actually overflows, and toggles
   is-at-start / is-at-end as the user scrolls so the relevant arrow hides
   at each end. Positioned over the edge fades (higher z-index) with
   pointer-events re-enabled (the fades themselves stay click-through). */
.ptbsec__arrow {
  position: absolute;
  top: 0; bottom: 0;
  width: 32px;
  z-index: 3;
  display: none;               /* revealed by .has-overflow below */
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  cursor: pointer;
  color: var(--lss-navy-700, #17203e);
  -webkit-appearance: none;
  appearance: none;
}
.ptbsec__arrow--left  { left: 0; }
.ptbsec__arrow--right { right: 0; }
/* Opaque-to-transparent wash so the arrow reads clearly over chips that
   scroll beneath it, blending into the strip's white background. */
.ptbsec__arrow--left  { background: linear-gradient(to right, #fff 55%, rgba(255,255,255,0)); }
.ptbsec__arrow--right { background: linear-gradient(to left,  #fff 55%, rgba(255,255,255,0)); }
.ptbsec__arrow svg { width: 20px; height: 20px; }
.ptbsec__arrow:hover   { color: var(--lss-orange-500, #ef6a2e); }
.ptbsec__arrow:active  { transform: scale(.92); }
/* Show the arrows only when there's overflow, and only on the side that
   still has content to reveal. The matching edge fade already hides at the
   same breakpoints, so arrow + fade appear/disappear together. */
.ptbsec.has-overflow .ptbsec__arrow { display: inline-flex; }
.ptbsec.has-overflow.is-at-start .ptbsec__arrow--left  { display: none; }
.ptbsec.has-overflow.is-at-end   .ptbsec__arrow--right { display: none; }
/* Keep the first/last chip clear of the arrows so they're never permanently
   masked. Only applies while overflow arrows are present. */
.ptbsec.has-overflow .ptbsec__scroll { padding-left: 38px; padding-right: 38px; }

/* Phone — the topbar shrinks to 56px, so lower the sticky top offset.
   Also tighten paddings and hide group labels (space is precious;
   dividers still cluster related chips). */
@media (max-width: 820px) {
  .ptbsec {
    top: calc(56px + env(safe-area-inset-top, 0px));
  }
  .ptbsec__scroll { padding: 6px 14px; gap: 5px; }
  .ptbsec__grouplbl { display: none; }
  .ptbsec__sep { height: 18px; margin: 0 2px; }
  .ptbsec__chip { padding: 6px 10px; font-size: 12.5px; min-height: 34px; }
  .ptbsec__chip-lbl { max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
}

/* When the icon rail is engaged on desktop, keep the strip aligned. */
body.psb-rail .ptbsec__scroll { padding-left: 20px; }

/* ==============================================================
   Frequently used — centered chip strip under the topbar (2026-07-09).
   Replaces the sidebar "Frequently used" section and the admin
   horizontal-scroll strip. Shown for all roles. A small set of
   most-used pages, centered; wraps to a second row on narrow widths
   rather than scrolling, so every item is always reachable.
   ============================================================== */
/* @dead audit-2026-07-17 — entire .pfreq_star_ / .pfreq__star_ strip retired
   2026-07-10 (per includes/layout.php:1532 comment "Frequently used lives in
   the sidebar again"). Zero runtime refs. Kept commented pending 30-day soak.
.pfreq {
  position: sticky;
  top: 60px;
  z-index: 29;
  background: #fff;
  border-bottom: 1px solid var(--border-subtle);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 10px;
  padding: 8px 20px;
}
.pfreq__lbl {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-faint, #7a809b);
  white-space: nowrap;
}
.pfreq__row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
}
.pfreq__chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 14px;
  border-radius: 999px;
  background: var(--lss-ink-50, #f5f6fa);
  border: 1px solid var(--border-subtle, #e7e9ef);
  color: var(--lss-navy-700, #17203e);
  font-size: 13px;
  font-weight: 600;
  line-height: 1.2;
  text-decoration: none;
  white-space: nowrap;
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}
.pfreq__chip:hover,
.pfreq__chip:focus-visible {
  background: #fff;
  border-color: var(--lss-navy-300, #b6bed4);
  color: var(--lss-navy-800, #0e1a34);
  outline: none;
}
.pfreq__chip:focus-visible { box-shadow: 0 0 0 2px var(--lss-navy-300, #b6bed4); }
.pfreq__chip.is-active {
  background: var(--lss-navy-700, #17203e);
  border-color: var(--lss-navy-700, #17203e);
  color: #fff;
}
.pfreq__chip-ic { display: inline-flex; width: 14px; height: 14px; align-items: center; justify-content: center; opacity: .85; }
.pfreq__chip-ic svg { width: 14px; height: 14px; }
.pfreq__chip.is-active .pfreq__chip-ic { opacity: 1; }
.pfreq__chip-badge {
  display: inline-flex;
  min-width: 18px;
  padding: 0 5px;
  height: 18px;
  align-items: center;
  justify-content: center;
  background: var(--lss-emergency, #c43d30);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  border-radius: 10px;
  margin-left: 2px;
}
@media (max-width: 820px) {
  .pfreq { top: calc(56px + env(safe-area-inset-top, 0px)); gap: 6px; padding: 6px 12px; }
  .pfreq__lbl { display: none; }
  .pfreq__chip { padding: 6px 11px; font-size: 12.5px; }
}
*/

/* ============================================================
   Admin mega menu (2026-07-09 evening).

   Six click-toggled dropdown triggers sitting in a sticky strip just
   below the topbar. Rendered ONLY for role=admin (server-side gate in
   layout.php); non-admins never see this element at all.

   Structure recap:
     .pmega                          — sticky bar wrapper
       .pmega__row                   — flex row of triggers
         .pmega__group               — one per admin category
           .pmega__trigger           — click-toggled button
           .pmega__panel             — dropdown (position: absolute)
             .pmega__cols            — flex row of link columns
               .pmega__col
                 .pmega__col-hd      — small-caps column heading (optional)
                 .pmega__link        — icon + label + optional badge

   Desktop only. On mobile (≤820px) the whole strip is display:none —
   the mobile experience is the hamburger drawer, which shows every
   admin section (because the sidebar-hide rule below is also gated to
   desktop). Every URL survives in both surfaces.
   ============================================================ */
.pmega {
  position: sticky;
  top: 60px;                  /* stacks under the 60px topbar */
  z-index: 28;                /* below topbar (30) + role-switcher menu,
                                 above .pfreq (29) so panels paint over it */
  background: #fff;
  border-bottom: 1px solid var(--border-subtle);
}
.pmega__row {
  display: flex;
  align-items: stretch;
  gap: 2px;
  padding: 0 22px;
  overflow: visible;          /* panels absolute-position out of this */
  min-height: 44px;
}
.pmega__group { position: relative; display: flex; align-items: stretch; }

/* --- Trigger button ---------------------------------------------- */
.pmega__trigger {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0 14px;
  height: 44px;
  border: 0;
  background: transparent;
  color: var(--lss-navy-700, #232160);
  font-family: var(--font-sans);
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
  border-radius: 0;
  border-bottom: 2px solid transparent;
  transition: background .12s, color .12s, border-color .12s;
  white-space: nowrap;
}
.pmega__trigger:hover {
  background: var(--lss-ink-50, #f5f6fa);
  color: var(--lss-navy-900, #0e1a34);
}
.pmega__trigger:focus-visible {
  outline: 2px solid var(--lss-navy-400, #6a71a8);
  outline-offset: -2px;
}
.pmega__trigger[aria-expanded="true"] {
  background: var(--lss-ink-50, #f5f6fa);
  color: var(--lss-navy-900, #0e1a34);
  border-bottom-color: var(--lss-orange-500, #ef6a2e);
}
/* is-active = the current page's section — subtle underline hint even
   when the panel is closed so admins can see where they are. */
.pmega__trigger.is-active {
  border-bottom-color: var(--lss-orange-500, #ef6a2e);
  color: var(--lss-navy-900, #0e1a34);
}
.pmega__caret {
  width: 12px;
  height: 12px;
  transition: transform .18s ease;
  opacity: .7;
}
.pmega__trigger[aria-expanded="true"] .pmega__caret { transform: rotate(180deg); opacity: 1; }

/* --- Dropdown panel ---------------------------------------------- */
.pmega__panel {
  position: absolute;
  top: 100%;
  left: 0;
  margin-top: 0;
  background: #fff;
  border: 1px solid var(--border-subtle);
  border-top: 0;               /* trigger's bottom border acts as the seam */
  border-radius: 0 0 10px 10px;
  box-shadow: 0 12px 32px rgba(6,16,40,.14);
  padding: 14px 16px 16px;
  min-width: 240px;
  z-index: 40;                  /* above topbar because it's a child of a
                                   later stacking context (.pmain), so we
                                   just need to beat siblings */
}
.pmega__panel[hidden] { display: none; }
/* Multi-column panels are wider — grow up to ~900px total. Single-column
   panels stay compact (240–320px). */
.pmega__panel--multi { min-width: 560px; }

.pmega__cols {
  display: flex;
  flex-wrap: wrap;
  gap: 22px;
  align-items: flex-start;
}
.pmega__col {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 200px;
  flex: 1 1 200px;
  max-width: 260px;
}
.pmega__col-hd {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-faint, #6b7392);
  padding: 4px 8px 6px;
  border-bottom: 1px solid var(--border-subtle);
  margin-bottom: 4px;
}
.pmega__link {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 8px;
  border-radius: 8px;
  color: var(--text-strong, #0e1a34);
  text-decoration: none;
  font-size: 13.5px;
  font-weight: 500;
  line-height: 1.25;
  transition: background .12s, color .12s;
  position: relative;
}
.pmega__link:hover {
  background: var(--lss-ink-50, #f5f6fa);
  color: var(--lss-navy-900, #0e1a34);
}
.pmega__link:focus-visible {
  outline: 2px solid var(--lss-navy-400, #6a71a8);
  outline-offset: 1px;
  background: var(--lss-ink-50, #f5f6fa);
}
.pmega__link.is-active {
  background: color-mix(in srgb, var(--lss-navy-700, #232160) 8%, #fff);
  color: var(--lss-navy-900, #0e1a34);
  font-weight: 600;
}
.pmega__link-ic {
  display: inline-flex;
  width: 18px;
  height: 18px;
  align-items: center;
  justify-content: center;
  flex: none;
  color: var(--lss-navy-600, #3a3a7a);
  opacity: .85;
}
.pmega__link-ic svg { width: 18px; height: 18px; }
.pmega__link.is-active .pmega__link-ic { opacity: 1; color: var(--lss-orange-500, #ef6a2e); }
.pmega__link-lbl { flex: 1; min-width: 0; }
.pmega__link-badge {
  display: inline-flex;
  min-width: 18px;
  padding: 0 6px;
  height: 18px;
  align-items: center;
  justify-content: center;
  background: var(--lss-emergency, #c43d30);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  border-radius: 10px;
}

/* --- Sidebar interaction ----------------------------------------
   On desktop, when body.has-admin-mega is present, the six admin
   secondary sections (People / Training / Customer-facing / Communication
   / Finance / System) are DUPLICATE renders — they exist in the
   sidebar (data source) and in the mega menu (fresh markup). Hide the
   sidebar copy so the admin's left rail stays short (Operations +
   Teaching (only when in Training's Teaching subsection remains empty
   for now — Teaching lives inside Training's mega panel) + Account).
   On mobile the sidebar becomes the whole nav again; the rule below
   turns off there so all sections are reachable via the drawer.
   ============================================================== */
@media (min-width: 821px) {
  body.has-admin-mega .psb__group[data-section="people"],
  body.has-admin-mega .psb__group[data-section="training"],
  body.has-admin-mega .psb__group[data-section="external"],
  body.has-admin-mega .psb__group[data-section="communication"],
  body.has-admin-mega .psb__group[data-section="finance"],
  body.has-admin-mega .psb__group[data-section="system"],
  /* 2026-07-12: instructor + local roles now carry the mega class too —
     their extra section slugs join the desktop sidebar-hide list. */
  body.has-admin-mega .psb__group[data-section="certifications"],
  body.has-admin-mega .psb__group[data-section="forms"],
  body.has-admin-mega .psb__group[data-section="reports"] {
    display: none;
  }
  /* .pfreq sits under the mega menu on desktop admin — bump its
     sticky top from 60px (topbar) to 60+44=104px so it clears the
     mega row. (2026-07-10: .pfreq retired; rule kept harmless.) */
  body.has-admin-mega .pfreq { top: 104px; }
}

/* --- 2026-07-10 swap: mega menu IN the topbar, breadcrumb below ---
   .pmega--tb is the same mega menu rendered as a flex child of .ptb
   (where the breadcrumb used to sit). The standalone-strip styling
   (sticky / border / background) is neutralised; triggers stretch to
   the topbar's height so the orange active underline sits on the
   topbar's bottom edge. Panels still absolute-position from each
   .pmega__group, dropping below the topbar. */
.pmega--tb {
  position: static;            /* the topbar itself is the sticky element */
  border-bottom: 0;
  background: transparent;
  /* 2026-07-16 overlap fix: was `flex: 1 1 auto`, which grew the mega to
     fill the topbar and — combined with nowrap + overflow:visible on the
     row — let the six triggers spill OVER .ptb__right (the account
     cluster) at every width below ~1466px, i.e. on every normal laptop
     (People…System printed on top of "Signed in as" / "Sign out").
     `0 1 auto` instead takes only the natural trigger width and SHRINKS
     into whatever space is left of the account cluster (which stays
     pinned right via its own margin-left:auto). No grow → no spill. */
  flex: 0 1 auto;
  min-width: 0;
  align-self: stretch;
}
.pmega--tb .pmega__row {
  padding: 0;
  min-height: 0;
  height: 100%;
  /* Must stay fully visible: any scrolling overflow value would also
     force overflow-y to auto and clip the dropdown panels (the "menus
     show nothing" bug, 2026-07-10). Narrow windows squeeze triggers
     instead. */
  overflow: visible;
  flex-wrap: nowrap;
}
.pmega--tb .pmega__trigger { padding: 0 8px; height: 100%; min-width: 0; }
/* Let triggers shrink (and ellipsis their labels) when the row runs out of
   room, rather than overflowing the account cluster. The row keeps
   overflow:visible so dropdown panels are never clipped — only the label
   text truncates, and only once space is genuinely tight (≈below 1300px).
   On wider laptops every label shows in full. */
.pmega--tb .pmega__row { min-width: 0; }
.pmega--tb .pmega__group { min-width: 0; }
.pmega--tb .pmega__trigger-lbl {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 2026-07-16: between the phone breakpoint (≤820px, handled above) and
   ~1500px the topbar mega menu and the account cluster share one tight
   line. Drop the redundant "Signed in as <role>" chip in that range — the
   adjacent "View as: <role>" switcher already names the active role — so
   the six mega triggers keep their full labels instead of ellipsising.
   Shown again ≥1500px, where there's room for both and it still carries
   the real identity during an admin "preview as" impersonation. */
@media (min-width: 821px) and (max-width: 1500px) {
  body.has-admin-mega .ptb__role { display: none; }
}

/* Breadcrumb bar — the trail the topbar gave up, now a slim sticky
   row in the mega menu's old slot (admins only; non-admins keep the
   breadcrumb inside the topbar). */
/* Not a chrome bar: a quiet, right-aligned trail floating on the page
   background above the content (the earlier full-width white strip
   read as a second toolbar — 2026-07-10 feedback). Non-sticky; it
   scrolls away with the page. */
.pcrumbbar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  padding: 10px 26px 0;
  font-size: 12.5px;
  white-space: nowrap;
  color: var(--text-muted, #5b6675);
  opacity: .85;
}
.pcrumbbar .ptb__crumb-role { font-size: 12.5px; }
.pcrumbbar .ptb__crumb-page { font-size: 12.5px; }
@media (max-width: 820px) {
  .pcrumbbar { padding: 8px 14px 0; }
}

/* --- Panel-open scrim + reduced motion --------------------------- */
@media (prefers-reduced-motion: reduce) {
  .pmega__caret { transition: none; }
  .pmega__trigger { transition: none; }
  .pmega__link { transition: none; }
}

/* --- Mobile fallback --------------------------------------------
   Hide the whole strip on the phone breakpoint. The hamburger takes
   over — every admin section is still in the sidebar drawer (the
   @media (min-width: 821px) sidebar-hide rule above is gated to
   desktop only, so mobile sees the complete section list). Nothing
   is emitted for non-admin roles at any width. */
@media (max-width: 820px) {
  .pmega { display: none; }
}

/* ============================================================
   Title Case across UI titles (2026-07-12, by request).
   text-transform: capitalize uppercases the first letter of each
   word and never lowercases anything — so "My data & privacy"
   renders "My Data & Privacy" while acronyms (ACLS, EMS, QR)
   and already-cased dynamic content (names, course titles) are
   untouched. Applied to navigation labels, breadcrumbs, page +
   panel + drawer headings, and footer links; body copy, buttons
   and form fields keep their authored casing.
   ============================================================ */
.psb__label,
.pmega__btn,
.pmega__link-lbl,
.ptb__crumb-role,
.ptb__crumb-sec,
.ptb__crumb-page,
.pcrumbbar a,
.pcrumbbar span,
.phead h1,
.phead h2,
.ppanel__head h1,
.ppanel__head h2,
.ppanel__head h3,
.ppanel > h3,
.pdrawer__head h2,
.psb__subhead,
/* CMDK_TITLE_CASE_2026-08-03 — "Jump to page" rows, so the palette matches
   the sidebar it mirrors. Kept in sync with TT_SELECTOR in portal.js. */
.cmdk__row-label,
.papp-footer__link span:last-child,
/* Buttons + filter chips (2026-07-12 follow-up): same Title Case.
   TT_ANCHOR_SYNC_2026-09-27: `button` catches a <button> whatever class
   it carries, but a LINK styled as a button matches only by class — which
   is why .lss-btn is named here. Any new button-like class has to be
   added in BOTH places (here and TT_SELECTOR in assets/js/portal.js) or
   its anchors render in the case they were authored in, next to buttons
   the rule has already Title Cased. That is what .nbtn did: "Open link"
   beside "View Hours" in the same notification row. */
button,
.lss-btn,
.nbtn,
input[type="submit"],
input[type="button"],
.doc-chip,
.lss-offline-ready-chip__label,
/* Modal / dialog titles ("Request a course", template pickers, etc.) */
.pmodal__head h1, .pmodal__head h2, .pmodal__head h3,
.pmodal h2[id$="-title"],
.ft-modal__head h2,
.dz-modal__card h2 {
  text-transform: capitalize;
}

/* BUTTON_KEEP_CASE_2026-09-25 — opt one run of text out of the Title Case rule
   above. The rule capitalises every word inside a button, which is right for a
   label and wrong for a value printed in one: an email address rendered
   "Aaron@lifesupportservices.ca" reads as a person's name, and a code or a
   filename would fare no better. Wrap the value, not the label. */
.keep-case { text-transform: none !important; }

/* JUMPTO_CASE_FLICKER_2026-08-04 — the sidebar's "Jump to page…" button is
   caught by the bare `button` selector above, so the CSS layer painted it
   "Jump To Page…" on first paint and the smart-title-case JS (portal.js,
   TT_SELECTOR sweep) then rewrote it back — its small-word list keeps "to"
   lowercase. Every navigation is a fresh paint, so the label visibly
   changed case on every nav click. Opt the button out here so both layers
   agree from the first frame; the JS pass still runs and is a no-op. */
.psb__search,
.psb__search .psb__search-label {
  text-transform: none;
}

/* ==============================================================
   .lss-filedrop — styled multi-file upload zone (2026-07-16)
   Reusable click-or-drag drop zone that replaces a bare
   <input type="file" multiple>. Mirrors the form-fill
   .lss-form__filedrop look (portal.css is loaded everywhere;
   forms.css is not), and adds a selected-files list with a
   per-file remove control. Progressive enhancement: the real
   <input> is layered invisibly over the zone, so with JS off the
   control still opens the OS picker and submits normally.
   Behaviour wired per-page via [data-filedrop] (see
   form-submission.php). ============================================ */
.lss-filedrop {
  position: relative;
  display: block;
  border: 2px dashed var(--border-default, #d8dae3);
  border-radius: 12px;
  background: #fafbfd;
  padding: 20px 18px;
  cursor: pointer;
  transition: border-color .15s ease, background .15s ease, box-shadow .15s ease;
}
.lss-filedrop:hover {
  border-color: var(--lss-navy-500, #3a3580);
  background: #f3f3fa;
}
.lss-filedrop.is-dragover {
  border-color: var(--lss-orange-500, #ef6a2e);
  background: var(--lss-orange-50, #fff4ec);
  box-shadow: inset 0 0 0 2px var(--lss-orange-500, #ef6a2e);
}
.lss-filedrop.has-file {
  border-style: solid;
  border-color: var(--lss-success, #1e7a44);
  background: #f1faf4;
}
.lss-filedrop__input {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  opacity: 0;
  cursor: pointer;
}
.lss-filedrop__input:focus-visible + .lss-filedrop__ui {
  box-shadow: var(--shadow-focus, 0 0 0 3px rgba(239, 106, 46, 0.18));
  border-radius: 10px;
}
.lss-filedrop__ui {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  pointer-events: none;          /* clicks fall through to the input */
  color: var(--text-body, #2a2f3a);
}
.lss-filedrop__icon {
  flex: none;
  width: 40px; height: 40px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: var(--lss-navy-50, #f3f3fa);
  color: var(--lss-navy-700, #232160);
  transition: background .15s ease, color .15s ease;
}
.lss-filedrop.is-dragover .lss-filedrop__icon { background: var(--lss-orange-500, #ef6a2e); color: #fff; }
.lss-filedrop.has-file  .lss-filedrop__icon  { background: var(--lss-success, #1e7a44); color: #fff; }
.lss-filedrop__text { min-width: 0; text-align: left; line-height: 1.35; }
.lss-filedrop__text b { color: var(--lss-navy-700, #232160); }
.lss-filedrop__browse { color: var(--lss-orange-500, #ef6a2e); font-weight: 600; text-decoration: underline; }
.lss-filedrop__text small { display: block; color: var(--text-muted, #5b6675); font-size: 11px; margin-top: 2px; }

/* Selected-files list (rendered by JS below the zone). */
.lss-filedrop__list { list-style: none; padding: 0; margin: 8px 0 0; display: flex; flex-direction: column; gap: 6px; }
.lss-filedrop__item {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 10px;
  border: 1px solid var(--border-subtle, #e6e8ef);
  border-radius: 8px;
  background: #fff;
  font-size: 12.5px;
}
.lss-filedrop__item-icon { flex: none; color: var(--lss-navy-500, #3a3580); display: inline-flex; }
.lss-filedrop__item-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-strong, #1c2230); }
.lss-filedrop__item-size { flex: none; color: var(--text-muted, #5b6675); font-size: 11.5px; }
.lss-filedrop__remove {
  flex: none;
  width: 24px; height: 24px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: 6px;
  background: transparent; color: var(--text-muted, #5b6675);
  cursor: pointer; font-size: 16px; line-height: 1;
  transition: background .12s ease, color .12s ease;
}
.lss-filedrop__remove:hover { background: #fdecec; color: #c0392b; }
.lss-filedrop__remove:focus-visible { outline: 2px solid var(--lss-navy-500, #3a3580); outline-offset: 1px; }

/* ============================================================
   IOS_FOCUS_ZOOM_2026-07-27 — form controls must be >= 16px on phones.

   iOS Safari auto-zooms the whole viewport the moment a form control
   with font-size < 16px receives focus. On modal-heavy pages (course
   types edit, users edit, settings) that zoom left the dialog wider
   than the screen — Andrew's "edit modal doesn't fit width-wise on
   mobile" report. The modal layout itself was fine; the zoom was the
   problem.

   16px at <=820px kills the auto-zoom while keeping pinch-zoom fully
   available (we deliberately do NOT set maximum-scale on the viewport
   meta — that would hurt accessibility). !important is required to
   beat the many inline font-size styles on inputs across the portal;
   checkboxes/radios are excluded because their rendered size doesn't
   trigger the zoom and some rely on em-scaling.
   ============================================================ */
@media (max-width: 820px) {
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]),
  select,
  textarea {
    font-size: 16px !important;
  }
}

/* ==========================================================================
 * DP_ROW_MOBILE_2026-08-11 — the day-picker's session rows.
 *
 * Andrew, from an iPhone: the date in each row was rendering one letter per
 * line. These styles used to be written inline by portal.js, where no media
 * query could reach them, and the five tracks -- 24px badge, flexible date,
 * two 100px time inputs, 34px remove -- came to 306px of fixed width, gap and
 * padding before the date column was given anything at all. Inside the card's
 * own padding on a phone there is no such room, so the date column collapsed
 * below its own longest word and broke per character.
 *
 * Below 620px the row becomes two lines: badge, date and remove across the
 * top, the two times beneath. Explicit grid placement rather than reordering
 * the DOM, so the tab order still runs date -> start -> end -> remove.
 * ========================================================================== */
.dp-day-row {
  display: grid;
  /* DP_TIME_MERIDIEM_2026-09-12 — these were 100px, and at 100px Chromium
     does not shrink a native <input type="time"> on a 12-hour device: it
     DROPS the AM/PM field altogether. A day booked 08:00-21:00 displayed as
     "08:00" and "09:00", which reads as a one-hour morning class. Nothing was
     clipped and nothing overflowed, so there was no visual hint either — the
     control simply stated the wrong time.

     Measured in Chromium at en-US: 100px shows no meridiem, 110px truncates
     to "09:00 P", 120px is the first width that renders "09:00 PM" whole.
     132px leaves room for a wider system font before it degrades again.

     The 24-hour readout beside the date (see CLOCK_24H_PICKER_2026-08-11) was
     correct throughout — it said 21:00. It is small grey text next to a large
     white input, so it lost. Worth remembering when the next "the label says
     it" fix is proposed: people read the control, not the annotation. */
  grid-template-columns: 24px minmax(0, 1fr) 132px 132px 34px;
  align-items: center;
  gap: 8px;
  padding: 8px;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background: #fafbfd;
}
.dp-day-row > * { min-width: 0; }

@media (max-width: 620px) {
  .dp-day-row {
    grid-template-columns: 24px minmax(0, 1fr) minmax(0, 1fr) 34px;
    row-gap: 8px;
  }
  .dp-day-row > *:nth-child(1) { grid-area: 1 / 1 / 2 / 2; }   /* day number  */
  .dp-day-row > *:nth-child(2) { grid-area: 1 / 2 / 2 / 4; }   /* the date    */
  .dp-day-row > *:nth-child(5) { grid-area: 1 / 4 / 2 / 5; }   /* remove      */
  .dp-day-row > *:nth-child(3) { grid-area: 2 / 2 / 3 / 3; }   /* start time  */
  .dp-day-row > *:nth-child(4) { grid-area: 2 / 3 / 3 / 4; }   /* end time    */
}

/* ---------- SHARED_PICKR_2026-09-19 ----------
   Moved out of calendar.php with the script it styles; the waitlist
   panel wears the same control now. */
/* SEARCH_AND_ADD_2026-09-18 */
.pickr { position:relative; }
.pickr__box { display:flex; flex-wrap:wrap; gap:5px; align-items:center;
              padding:6px 8px; min-height:38px; border-radius:8px;
              border:1px solid var(--border-subtle); background:#fff; cursor:text; }
.pickr__box:focus-within { border-color:var(--lss-navy-700,#232160);
              box-shadow:0 0 0 3px rgba(35,33,96,.10); }
.pickr__chip { display:inline-flex; align-items:center; gap:6px;
              background:#eef0f7; border:1px solid #ccd2e4; border-radius:7px;
              padding:2px 4px 2px 8px; font-size:12.5px; color:#26304a; max-width:100%; }
.pickr__chip span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.pickr__x { border:0; background:transparent; cursor:pointer; font-size:15px;
            line-height:1; padding:0 3px; color:#5a6a8a; border-radius:4px; }
.pickr__x:hover { background:#dde1ee; color:#1d2540; }
.pickr__in { flex:1; min-width:140px; border:0; outline:0; font:inherit;
             font-size:13.5px; padding:4px 2px; background:transparent; }
.pickr__menu { position:absolute; z-index:40; left:0; right:0; top:calc(100% + 4px);
               max-height:230px; overflow:auto; background:#fff;
               border:1px solid var(--border-subtle); border-radius:10px;
               box-shadow:0 10px 28px rgba(16,20,40,.14); padding:4px; }
.pickr__opt { padding:7px 10px; border-radius:7px; font-size:13.5px; cursor:pointer; }
.pickr__opt[aria-selected="true"] { background:var(--lss-ink-100,#f1f2f7); }
.pickr__none { padding:9px 10px; font-size:12.5px; color:var(--text-faint); }
.pickr__opt--new { border-top:1px solid var(--border-subtle); margin-top:3px;
                   color:var(--lss-navy-700,#232160); }
.pickr__opt--new b { font-weight:700; }
.pickr__chip.is-new { background:#eaf5ee; border-color:#c2e0cd; color:#1e5d33; }
.pickr__new { font-style:normal; font-size:10px; font-weight:700; text-transform:uppercase;
              letter-spacing:.05em; background:#2f8f4e; color:#fff;
              border-radius:4px; padding:1px 4px; }


/* ============================================================
   WAITLIST_STYLE_2026-09-20 — the course-waitlist blocks.
   Andrew, on waitlist.php: "its a lot of black and white with not a lot
   of visual sepeartion."

   The reason it read flat is that every accent the markup had was
   conditional on a state his data does not contain: the green card needs
   a minimum (all of his are 0) and the red and amber chips need somebody
   past or near a needed_by date (nobody has one). So seven identical
   white blocks stacked on a white panel, 12px apart, with a 1px grey
   rule between people.

   The separation here is therefore UNCONDITIONAL — a tinted head band,
   a white list, a sunken action foot, a coloured left stripe — and the
   state colours sit on top of that rather than being the only thing
   colouring a card. A page where nothing is urgent should still look
   organised.

   Shared surface: this panel also renders on the dashboard.
   ============================================================ */
.cwl-block {
  border:1px solid var(--border-subtle);
  /* TYPE_COLOUR_2026-09-20 — the stripe is the course type's own colour,
     handed in as --cwl-type by the renderer. It answers WHICH COURSE, and
     it answers it whether or not anything is urgent, which is the whole
     reason it is here: a page with no alerts on it still has seven
     distinguishable cards.

     So nothing about status touches it. A BLS list does not stop being
     blue because somebody's date went past — status gets the head band
     and the chips, which is plenty. */
  border-left:3px solid var(--cwl-type, var(--lss-navy-300, #8b89c9));
  border-radius:12px; background:#fff; overflow:hidden;
  margin-bottom:14px; box-shadow:0 1px 2px rgba(16,20,45,.05);
}

.cwl-block__head {
  display:flex; gap:10px; align-items:center; flex-wrap:wrap;
  padding:10px 14px;
  /* Same colour at a 7% wash, mixed in PHP — see course_waitlist_tint(). */
  background:var(--cwl-wash, var(--lss-navy-50, #f3f3fa));
  border-bottom:1px solid var(--cwl-edge, var(--border-subtle));
}
/* Status takes the band when there IS a status, because on the day
   somebody's ticket has lapsed that outranks knowing which course it was.
   is-over last: a list can be both ready and overdue, and the deadline is
   the half that needs somebody today. */
.cwl-block.is-ready .cwl-block__head { background:#eef8f2; border-bottom-color:#cfe6d9; }
.cwl-block.is-soon  .cwl-block__head { background:#fdf8ec; border-bottom-color:#f1d6a3; }
.cwl-block.is-over  .cwl-block__head { background:#fdf1ef; border-bottom-color:#f1bdb6; }
.cwl-block__title { font-size:14px; font-weight:700; color:var(--text-strong); }
.cwl-block__meta  { font-size:12.5px; color:var(--text-muted); }
.cwl-block__cta   { margin-left:auto; }

/* One pill shape for all four states, so "no minimum set" is a chip like
   the others instead of loose grey text trailing off the line. */
.cwl-chip {
  font-size:11.5px; font-weight:700; line-height:1.5;
  border-radius:999px; padding:2px 10px;
  border:1px solid transparent; white-space:nowrap;
}
.cwl-chip--ready { color:#1e7a44; background:#e6f5ed; border-color:#b9ddc8; }
.cwl-chip--over  { color:#9c2c1c; background:#fdecea; border-color:#f1bdb6; }
.cwl-chip--soon  { color:#8a5400; background:#fcf2dd; border-color:#f1d6a3; }
.cwl-chip--quiet { font-weight:600; color:var(--text-muted);
                   background:var(--lss-white, #fff); border-color:var(--border-subtle); }

.cwl-block__body { padding:4px 14px 10px; }
/* Negative margin + matching padding so the hover wash reaches past the
   card's text column without the names moving. */
.cwl-row {
  display:flex; gap:10px; align-items:center; flex-wrap:wrap;
  padding:7px 8px; margin:0 -8px; border-radius:7px;
  border-bottom:1px solid var(--lss-ink-100, #eef0f3);
}
.cwl-row:last-child { border-bottom:0; }
.cwl-row:hover { background:var(--lss-ink-50, #f7f8fa); }
.cwl-row__name { font-size:13px; font-weight:700; color:var(--text-strong); }
.cwl-row__meta { font-size:12px; color:var(--text-muted); }
.cwl-row__actions { margin-left:auto; display:flex; gap:6px;
                    align-items:center; flex-wrap:wrap; }

/* Demoted onto its own surface. The composer is the biggest thing in the
   block and the least often used; sinking it stops it reading as the
   point of the card. */
.cwl-block__foot {
  display:flex; gap:16px; flex-wrap:wrap;
  padding:12px 14px;
  background:var(--lss-ink-50, #f7f8fa);
  border-top:1px solid var(--border-subtle);
}
.cwl-foot__note { width:100%; margin:0; font-size:12.5px; color:var(--text-muted); }

@media (max-width: 720px) {
  /* auto margins have nothing to push against once everything is on its
     own line, and left them hanging mid-row. */
  .cwl-block__cta, .cwl-row__actions { margin-left:0; width:100%; }
  .cwl-row { align-items:flex-start; }
  .cwl-block__head { padding:10px 12px; }
  .cwl-block__body { padding:4px 12px 10px; }
  .cwl-block__foot { padding:12px; }
}
