/* ============================================================================
   hq-ui.css — foundation layer, loaded AFTER app.css.

   Why a second file rather than 1,100 more lines in the first: everything here is
   cross-cutting. It applies to all 169 pages without any of them being edited, which
   matters because there are ~2,300 inline style attributes in the markup and no
   component layer to change instead. Keeping it separate also means the whole layer
   can be reverted with one <link> tag if something regresses.

   Order inside the file: reset/type, accessibility, responsive, motion, components.
   ========================================================================== */


/* ============================================================================
   1. TYPE & RHYTHM
   ========================================================================== */

/* Fluid, capped. 15px on a phone and 16px on a desktop, without a breakpoint —
   clamp() interpolates between them. Everything sized in rem follows automatically. */
html{font-size:clamp(15px,.92rem + .18vw,16px);-webkit-text-size-adjust:100%}

body{line-height:1.55;text-rendering:optimizeLegibility}

/* Long clinical notes, addresses and expert bios were breaking layouts because nothing
   constrained them. */
p,li,dd{overflow-wrap:break-word}

/* Numbers that change in place — counters, prices, durations — were shifting their own
   width as digits changed, making dashboards twitch. Opt-in via [data-numeric] for table
   cells, since a blanket rule would also hit text columns where it looks wrong. */
.stat .n,.kv .val,.pkg-price-display b,.hq-count,
[data-numeric]{font-variant-numeric:tabular-nums}


/* ============================================================================
   2. ACCESSIBILITY
   This app handles PHI and is sold to corporate buyers, whose procurement asks
   about WCAG. Before this layer: role= appeared 0 times across 169 files, aria-*
   3 times, there was no skip link, and h1/h2/h3 had outline:none.
   ========================================================================== */

/* ---- Focus. A visible, consistent ring on anything reachable by keyboard.
   :focus-visible (not :focus) so a mouse click doesn't leave a ring behind — that
   is the reason people reach for outline:none in the first place. The double ring
   (card-coloured inner, accent outer) stays visible on every background in the
   app, including the dark sidebar and coloured banners. */
:where(a,button,input,select,textarea,summary,[tabindex],[role="button"],[role="tab"]):focus-visible{
  outline:none;
  box-shadow:var(--focus-ring);
  border-radius:var(--r-sm);
  position:relative;
  z-index:1;
}
/* Fields already own their focus treatment; keep it and add the ring on top of it. */
.field:focus-visible{box-shadow:0 0 0 4px var(--rose-soft), var(--focus-ring)}

/* Forced-colours (Windows High Contrast) throws away box-shadow, so the ring needs a
   real outline there or focus becomes invisible for the people most relying on it. */
@media (forced-colors:active){
  :where(a,button,input,select,textarea,[tabindex]):focus-visible{outline:3px solid Highlight;outline-offset:2px}
}

/* ---- Skip link. Every page put the same ~40-item sidebar before its content, so a
   keyboard or screen-reader user had to pass all of it on every single navigation.
   fixed, not absolute, so it still works if focus is restored after a scroll. */
.skip-link{
  position:fixed;left:50%;top:calc(var(--s-2) * -12);
  transform:translateX(-50%);
  z-index:calc(var(--z-toast) + 1);
  background:var(--accent);color:#fff;
  padding:var(--s-3) var(--s-5);border-radius:0 0 var(--r-md) var(--r-md);
  font-size:14px;font-weight:600;
  box-shadow:var(--e-3);
  transition:top var(--t-base) var(--ease-out);
}
/* Centred rather than tucked into the corner, because on mobile the corner is where the
   hamburger lives and the two would sit on top of each other. */
.skip-link:focus{top:0}

/* ---- Screen-reader-only text, for labels that would be visual clutter but that a
   non-visual user needs (icon-only buttons, table captions, live regions). */
.sr-only{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;
}
.sr-only-focusable:focus,.sr-only-focusable:focus-within{
  position:static;width:auto;height:auto;overflow:visible;clip:auto;clip-path:none;white-space:normal;
}

/* ---- Icon-only controls. Emoji and glyph buttons read as literal characters
   ("black telephone", "heavy multiplication x") without a label. */
button:not([aria-label]):not([title]):empty::after{content:"button"}

/* ---- Colour is never the only signal. Status pills carried meaning purely in hue,
   which excludes ~8% of men. A leading marker gives the same information without it. */
.pill.ok::before{content:"● ";}
.pill.pending::before{content:"◐ ";}
.pill.err::before,.pill.rejected::before{content:"✕ ";}

/* ---- Motion. Was one rule covering .reveal only; the login page's floating blobs,
   the spinning brand dot and every hover transform ran regardless. Vestibular
   disorders are triggered by exactly that kind of large, looping movement. */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.001ms!important;
    animation-iteration-count:1!important;
    transition-duration:.001ms!important;
    scroll-behavior:auto!important;
  }
  .login-page::before,.login-page::after,.brand .dot{animation:none!important}
  .reveal,.hq-stagger>*{opacity:1!important;transform:none!important}
}


/* ============================================================================
   3. RESPONSIVE
   The MAUI app reuses these same Razor components, so anything unfixed here ships
   into the phone app too.
   ========================================================================== */

/* ---- Tables. 18 files render a <table>; only 2 wrapped it in something scrollable,
   so 16 pages tore their layout open on a phone. Fixing that per page would mean
   editing markup in all of them; this does it once, for every table that exists now
   or is added later, without any of them being touched.

   display:block on the table is what makes overflow work at all — a <table> ignores
   overflow otherwise. It costs the column-width algorithm, which is why min-width
   forces the table to keep its natural desktop width and scroll rather than crush
   columns into unreadable slivers. */
@media (max-width:900px){
  table{display:block;overflow-x:auto;-webkit-overflow-scrolling:touch;max-width:100%}
  table > tbody,table > thead,table > tfoot{min-width:max-content}
  /* Scrolling has to be discoverable or it may as well not exist — this is the shadow
     that appears only when there is more table to the right. */
  table{
    background:
      linear-gradient(to right,var(--card) 30%,transparent) left/40px 100% no-repeat,
      linear-gradient(to left,var(--card) 30%,transparent) right/40px 100% no-repeat,
      radial-gradient(farthest-side at 0 50%,rgba(var(--shadow-ink),.14),transparent) left/14px 100% no-repeat,
      radial-gradient(farthest-side at 100% 50%,rgba(var(--shadow-ink),.14),transparent) right/14px 100% no-repeat;
    background-attachment:local,local,scroll,scroll;
  }
}

/* ---- Touch targets. Several .btn variants sat at padding:8px 14px — about 32px tall.
   Apple and Google both specify 44px, and sub-44px controls are most of why a web app
   feels "not quite native" on a phone. Applied only on coarse pointers so desktop
   density (which this admin tool depends on) is untouched. */
@media (pointer:coarse){
  .btn,.cta,button:not(.star):not(.mobile-nav-toggle),
  .role-choice-btn,.slotchip,.chip,select,input[type="checkbox"],input[type="radio"]{
    min-height:44px;
  }
  input[type="checkbox"],input[type="radio"]{min-width:24px;width:24px;height:24px}
  .btn,.cta{padding-inline:var(--s-4)}
  /* A tap target can be 44px without the element looking 44px — this keeps small
     inline actions visually compact while still being comfortably tappable. */
  .exp-actions .btn,.sess-actions .btn{display:inline-flex;align-items:center}
  /* iOS zooms the whole page when a focused input is under 16px. */
  input,select,textarea{font-size:max(16px,1em)}
}

/* ---- One breakpoint scale. The stylesheet used 480/600/640/700/800/900/1000 with no
   system behind which was which. New rules use these three; existing ones are left as
   they are rather than risk regressions across 169 pages for tidiness alone.
      sm  640  — phone
      md  900  — tablet / sidebar collapse (matches the existing nav breakpoint)
      lg 1200  — desktop
   ---------------------------------------------------------------------------- */

/* Content width was fixed at 1080px, which wastes half of a modern monitor on the
   data-dense admin tables this app is mostly made of. */
@media (min-width:1400px){
  .wrap{max-width:1200px}
  .wrap-wide{max-width:1500px}
}

/* Horizontal overflow anywhere shows as a page that rubber-bands sideways on a phone —
   easy to ship, hard to notice on a desktop. */
@media (max-width:640px){
  html,body{overflow-x:hidden}
  .wrap,.wrap-wide{padding-left:var(--s-4);padding-right:var(--s-4)}
  .topbar{padding-inline:var(--s-4)}
  .topbar h1{font-size:18px}
  .card{padding:var(--s-4);border-radius:var(--r-md)}
  .grid,.cols,.card-grid,.pkg-grid{grid-template-columns:1fr!important;gap:var(--s-3)}
  .modal-card{border-radius:var(--r-lg) var(--r-lg) 0 0}
}

/* Full-height layouts were using 100vh, which on iOS Safari means "the viewport with
   the address bar hidden" — so the bottom of the sidebar and the login screen sat
   under the browser chrome and could not be reached. dvh is the fix. */
@supports (height:100dvh){
  .hq-side{height:100dvh}
  .login-page{min-height:100dvh}
  .hq-app{min-height:100dvh}
}

/* Devices with a notch or a home indicator. The padding goes on .nav, not on the scroll
   container — padding on .hq-side sits outside the nav's painted area, which reopened the
   same two-colour band at the very bottom of the menu that .hq-side's background fixes. */
@supports (padding:max(0px)){
  .nav{padding-bottom:max(var(--s-5),env(safe-area-inset-bottom))}
  .fab-wrap,.crisis-fab{bottom:max(var(--s-5),env(safe-area-inset-bottom))}
}


/* ============================================================================
   4. MOTION
   What existed was decorative — floating blobs on the login page. What was missing
   was the kind that carries information: what is loading, what just arrived, what
   responded to a tap. That is the difference people read as "well built".
   ========================================================================== */

/* ---- Skeletons. 33 pages showed the word "Loading…". Text-as-loading-state reads as
   unfinished; a shape the size of the content that is coming reads as fast, because the
   layout stops jumping when data lands. */
.sk{
  background:linear-gradient(90deg,var(--line-2) 25%,var(--field) 37%,var(--line-2) 63%);
  background-size:400% 100%;
  animation:sk-shimmer 1.4s var(--ease-in-out) infinite;
  border-radius:var(--r-sm);
}
@keyframes sk-shimmer{from{background-position:100% 50%}to{background-position:0 50%}}
.sk-text{height:1em;margin:.35em 0}
.sk-text.w-40{width:40%}.sk-text.w-60{width:60%}.sk-text.w-80{width:80%}
.sk-title{height:1.5em;width:45%;margin-bottom:var(--s-4)}
.sk-avatar{width:44px;height:44px;border-radius:var(--r-md);flex:0 0 auto}
.sk-card{height:120px;border-radius:var(--r-lg)}
.sk-row{display:flex;gap:var(--s-3);align-items:center;padding:var(--s-3) 0}
.sk-row .sk-text{flex:1}

/* ---- Stagger. Lists arriving all at once read as a flash; arriving in sequence reads
   as being assembled. Capped at 10 because beyond that the last item feels late rather
   than deliberate. */
.hq-stagger>*{animation:hq-rise .38s var(--ease-out) both}
.hq-stagger>*:nth-child(1){animation-delay:.02s}
.hq-stagger>*:nth-child(2){animation-delay:.05s}
.hq-stagger>*:nth-child(3){animation-delay:.08s}
.hq-stagger>*:nth-child(4){animation-delay:.11s}
.hq-stagger>*:nth-child(5){animation-delay:.14s}
.hq-stagger>*:nth-child(6){animation-delay:.17s}
.hq-stagger>*:nth-child(7){animation-delay:.20s}
.hq-stagger>*:nth-child(8){animation-delay:.23s}
.hq-stagger>*:nth-child(9){animation-delay:.26s}
.hq-stagger>*:nth-child(n+10){animation-delay:.28s}
@keyframes hq-rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}

/* ---- Press feedback. Blazor Server round-trips to the server for every click, so
   without a local response the UI feels dead for the length of the network hop —
   the single biggest reason a Blazor Server app can feel sluggish when it isn't. */
@media (prefers-reduced-motion:no-preference){
  .btn,.cta,.chip,.slotchip,.role-choice-btn,.nav a{
    transition:transform var(--t-fast) var(--ease-spring),
               background-color var(--t-fast) linear,
               box-shadow var(--t-base) var(--ease-out);
  }
  .btn:active:not(:disabled),.cta:active:not(:disabled),
  .chip:active,.slotchip:active,.role-choice-btn:active{transform:scale(.97)}
}

/* ---- Busy state. A disabled button during a save tells you nothing about why. */
[aria-busy="true"],.is-busy{position:relative;pointer-events:none;color:transparent!important}
[aria-busy="true"]::after,.is-busy::after{
  content:"";position:absolute;inset:0;margin:auto;
  width:16px;height:16px;border-radius:50%;
  border:2px solid currentColor;border-top-color:transparent;
  color:#fff;animation:hq-spin .6s linear infinite;
}
.btn.ghost[aria-busy="true"]::after,.btn.ghost.is-busy::after{color:var(--ink)}
@keyframes hq-spin{to{transform:rotate(360deg)}}


/* ============================================================================
   5. COMPONENT REFINEMENT
   Surface-level only. Everything below reuses classes the markup already applies,
   so no page needs editing to pick it up.
   ========================================================================== */

/* Cards: a hairline plus a genuine two-layer shadow instead of the flat 1px one.
   Calm-clinical means restraint — this is deliberately close to flat when at rest. */
.card,.stat,.exp-card,.pkg-card,.phead{
  box-shadow:var(--e-1);
  transition:box-shadow var(--t-base) var(--ease-out),border-color var(--t-base) var(--ease-out);
}
@media (hover:hover){.card:hover,.stat:hover{box-shadow:var(--e-2)}}
@media (hover:hover){.exp-card:hover,.pkg-card:hover{box-shadow:var(--e-3);border-color:var(--line)}}

/* Section headings sat at the same weight as body text in several places. */
.card h3{letter-spacing:-.005em;color:var(--ink)}

/* The sidebar's active item was a solid accent block — heavy, and it fought every
   other accent on the page. A rail plus a tinted ground is quieter and still
   unambiguous, and it survives a theme change without a contrast problem. */
.nav a.active{
  background:var(--accent-soft);
  color:var(--accent);
  font-weight:600;
  position:relative;
}
.nav a.active::before{
  content:"";position:absolute;left:0;top:20%;bottom:20%;
  width:3px;border-radius:0 var(--r-pill) var(--r-pill) 0;background:var(--accent);
}
.nav a{transition:background-color var(--t-fast) linear,color var(--t-fast) linear}

/* The sticky header's border was a hard 1px line that read as a box edge. A hairline plus
   a very short ambient shadow reads as a surface floating above the content instead.
   (A scroll-aware border that only appears once content is behind it would be better
   still, but it needs either a JS listener or scroll-driven animations, which Safari and
   Firefox do not ship yet — so this is the version that actually works everywhere.) */
.topbar{
  border-bottom-color:var(--line-2);
  box-shadow:0 1px 0 rgba(var(--shadow-ink),.03), 0 4px 12px -8px rgba(var(--shadow-ink),.10);
}

/* Table legibility lives in section 11, with the rest of the admin list styling — these
   rules were written here first and then again there, which is exactly the duplication this
   whole exercise is meant to remove. .simple-table picks up the same treatment there. */

/* Empty states: there is already an <EmptyState /> component with its own styles in
   app.css (.empty-state-icon, h3, .empty-state-actions). A parallel set was written here
   before that was noticed, which silently overrode its padding and paragraph width while
   adding .ico/b rules for elements the component never renders. Removed — the existing
   one is correct and this file should not be quietly reinterpreting it. */

/* Anything that scrolls inside the page keeps a slim, theme-aware scrollbar rather
   than the OS default, which on Windows is a wide grey slab. */
.hq-side,.msg-contacts,[class*="-scroll"]{scrollbar-width:thin;scrollbar-color:var(--line) transparent}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-thumb{background:var(--line);border-radius:var(--r-pill);border:3px solid transparent;background-clip:content-box}
@media (hover:hover){::-webkit-scrollbar-thumb:hover{background:var(--navy-2);background-clip:content-box}}
::-webkit-scrollbar-track{background:transparent}

/* Selection in the app's own accent rather than the browser's default blue. */
::selection{background:var(--accent-soft);color:var(--accent)}

/* Print — invoices, prescriptions and reports are printed from this app, and they were
   printing the sidebar, the floating assistant and the crisis button with them. */
@media print{
  .hq-side,.mobile-nav-toggle,.nav-backdrop,.topbar,.hq-back-bar,
  .fab-wrap,.crisis-fab,.skip-link,[class*="floating"]{display:none!important}
  .hq-app{display:block}
  .wrap,.wrap-wide{max-width:none;padding:0}
  .card{box-shadow:none;border:1px solid #ddd;break-inside:avoid}
  a[href^="http"]::after{content:" (" attr(href) ")";font-size:11px;color:#555}
}


/* ============================================================================
   6. UTILITY LAYER

   The markup carried ~2,300 inline style attributes and no component layer, which
   made "change the corner radius everywhere" a 2,300-place find-and-replace instead
   of one edit. 603 distinct style strings turned out to be 76% expressible as a
   small set of names — spacing, text, flex — so those became classes and the markup
   was migrated onto them. The remaining 24% are genuinely one-off (width:70px,
   object-fit:cover) and stay inline, which is the correct place for them.

   Spacing snaps to the 4pt scale in app.css. Two values in the old markup were off
   it — 10px and 14px — and map UP to 12px and 16px. That is a deliberate 2px shift
   in ~300 places: invisible on its own, and the entire reason to have a scale.

   Deliberately NOT a full utility framework. Every class here earns its place by
   replacing at least ten occurrences; anything rarer stays inline where it is
   easier to read in context.
   ========================================================================== */

/* ---- Spacing ---- */
.m-0{margin:0}
.p-0{padding:0}
.mt-0{margin-top:0}
.mt-1{margin-top:var(--s-1)}
.mt-2{margin-top:var(--s-2)}
.mt-3{margin-top:var(--s-3)}
.mt-4{margin-top:var(--s-4)}
.mt-5{margin-top:var(--s-5)}
.mt-6{margin-top:var(--s-6)}
.mb-1{margin-bottom:var(--s-1)}
.mb-2{margin-bottom:var(--s-2)}
.mb-3{margin-bottom:var(--s-3)}
.mb-4{margin-bottom:var(--s-4)}
.mb-5{margin-bottom:var(--s-5)}
.mb-6{margin-bottom:var(--s-6)}
.my-2{margin-block:var(--s-2)}
.ml-auto{margin-left:auto}

/* ---- Text ----
   Sizes collapse the old set (11/11.5/12/12.5/13/13.5/14px — seven sizes doing the
   work of three) onto a type scale. Nothing in an interface needs a half-pixel
   distinction nobody can perceive. */
.t-xs{font-size:12px}
.t-sm{font-size:13px}
.t-base{font-size:14px}
.t-muted{color:var(--muted)}
.t-body{color:var(--navy-2)}
.t-err{color:var(--err)}
.t-ok{color:var(--ok)}
.t-accent{color:var(--gold-text)}
.t-center{text-align:center}
.t-right{text-align:right}
.fw-400{font-weight:400}
.fw-600{font-weight:600}
.fw-700{font-weight:700}
.lh-loose{line-height:1.7}
.mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;font-size:.92em}

/* ---- Layout ---- */
.flex{display:flex}
.flex-wrap{flex-wrap:wrap}     /* NOT .wrap — that name is already the page container in app.css,
                                  which carries max-width and page padding. Anything given .wrap
                                  as a flex utility would have silently inherited both. */
.items-center{align-items:center}
.items-start{align-items:flex-start}
.between{justify-content:space-between}
.grow{flex:1}
.gap-2{gap:var(--s-2)}
.gap-3{gap:var(--s-3)}
.gap-4{gap:var(--s-4)}
.w-full{width:100%}
.pointer{cursor:pointer}
.no-border{border:none}

/* ---- State ----
   background + border-color in the accent tint appeared as a pair 31 times and always
   meant the same thing: this is the option you picked. */
.is-picked{background:var(--gold-soft);border-color:var(--gold-soft)}

/* ---- Composite ----
   These are the shapes that repeated most across pages, named for what they are rather
   than what they are made of. */
.hint-text{color:var(--muted);font-size:13px;margin:0;line-height:1.6}
.row-between{display:flex;justify-content:space-between;align-items:center;gap:var(--s-3)}
.actions{display:flex;gap:var(--s-3);margin-top:var(--s-4);flex-wrap:wrap}


/* ============================================================================
   7. TOUCH & WEBVIEW HARDENING

   The MAUI app renders these same pages in a WebView, so anything left unhandled
   here does not stay a web problem — it ships as an app problem, where users judge
   it against native apps rather than against websites.
   ========================================================================== */

/* ---- Tap highlight. Android and older iOS WebViews paint a translucent grey or blue
   rectangle over anything tapped, sized to the element box rather than its visual
   shape — so a pill-shaped button flashes as a square. The :active states defined
   earlier are the intended feedback; this removes the one the platform adds. */
a,button,[role="button"],.chip,.slotchip,.tag,.pill,label,summary{
  -webkit-tap-highlight-color:transparent;
}

/* ---- Text selection. Long-pressing a button or a nav item pops the selection
   handles and a copy menu, which never makes sense on a control. Content keeps
   selection — people copy phone numbers, invoice IDs and prescription text. */
.nav,.topbar,.btn,.cta,.pill,.badge,.chip,.tag,.mobile-nav-toggle,
.wiz-steps,.stat .l,.skip-link{
  -webkit-user-select:none;user-select:none;
}
p,td,pre,code,.kv .val,.doc,.hint-text,input,textarea,[data-selectable]{
  -webkit-user-select:text;user-select:text;
}

/* ---- Scroll chaining. Scrolling to the end of a modal, the notification dropdown or
   the sidebar handed the gesture to the page behind it — so the page scrolled while a
   modal was open, and in a WebView the whole app view bounced or triggered
   pull-to-refresh mid-interaction. */
.modal-card,.notif-dropdown,.fab-log,.chat-log,.hq-side,.med-results,
.rx-history,.transcript-log,.checkbox-list,.terms-scroll{
  overscroll-behavior:contain;
}
body{overscroll-behavior-y:none}

/* ---- Momentum scrolling for older iOS WebViews. */
.hq-side,.modal-card,.chat-log,.fab-log,table{-webkit-overflow-scrolling:touch}

/* ---- Double-tap zoom. A 300ms delay before click fires on anything the browser thinks
   might be double-tapped — the classic "the app feels laggy" that isn't the network. */
a,button,input,select,textarea,[role="button"],.chip,.slotchip{touch-action:manipulation}

/* ---- backdrop-filter. Cheap on iOS, expensive on Android WebView, where a blurred
   sticky header repaints on every scroll frame and drops the whole list to ~30fps.
   The header stays legible with a solid background instead. */
@media (max-width:900px){
  .topbar{backdrop-filter:none;-webkit-backdrop-filter:none;background:var(--card)}
}

/* ---- The on-screen keyboard. A fixed-position action bar sits under the keyboard on
   Android, which puts the submit button somewhere unreachable. Anchoring to the small
   viewport keeps it above. */
@supports (height:100svh){
  @media (max-width:900px){
    .modal-overlay{height:100svh}
    .fab-panel{max-height:70svh}
  }
}

/* ---- Pointer-coarse hit area for close buttons, which are consistently the smallest
   and most-missed control in any modal. */
@media (pointer:coarse){
  .modal-card > button:first-child{min-width:44px;min-height:44px}
}

/* ---- Landscape phones. The sidebar drawer at 270px eats most of a 640px-tall
   landscape viewport; and stat grids at four columns become unreadable slivers. */
@media (max-height:480px) and (orientation:landscape){
  .hq-side{width:min(270px,60vw)}
  .modal-card{max-height:92vh;padding:var(--s-4)}
  .stats{grid-template-columns:repeat(2,1fr)}
}

/* ---- Fonts. The app requests Inter from Google's CDN, which in a mobile app means a
   network round-trip to a third party before text can paint with the right face — and on
   a slow or absent connection, either a long swap or no swap at all. system-ui is the
   fallback so the first paint is always correct-ish, and for the packaged app the font
   should be bundled locally rather than fetched. Documented here because the change
   belongs in the MAUI project, not in this stylesheet. */


/* ============================================================================
   8. SAFETY-CRITICAL UI
   Crisis support is the one control in this product that must never be the thing
   that failed. It gets rules that no theme, page or animation can override.
   ========================================================================== */

.crisis-fab,.crisis-link{
  /* Never dimmed by a reduced-motion reset, a busy state or a disabled parent. */
  opacity:1!important;
  pointer-events:auto!important;
}
@media (pointer:coarse){
  .crisis-fab{min-width:52px;min-height:52px}   /* above the 44px floor, deliberately */
}


/* ---- Boot screen -----------------------------------------------------------------
   Shown while the stored session is restored, before any page renders. Deliberately
   almost nothing: a page-coloured field and one quiet mark. A spinner here would be a
   promise about duration that this screen cannot make — it is usually gone in under
   200ms, and a spinner that flashes for 200ms reads as a glitch. */
.boot-screen{
  min-height:100vh;display:flex;align-items:center;justify-content:center;
  background:var(--page);
}
@supports (height:100dvh){ .boot-screen{min-height:100dvh} }
.boot-mark{
  width:34px;height:34px;border-radius:var(--r-md);
  background:var(--accent);
  animation:boot-pulse 1.6s var(--ease-in-out) infinite;
}
@keyframes boot-pulse{0%,100%{opacity:.35;transform:scale(.94)}50%{opacity:1;transform:scale(1)}}
@media (prefers-reduced-motion:reduce){ .boot-mark{animation:none;opacity:.8} }

/* Skeletons need a container so the wrapper does not collapse or inherit a flex context
   that squashes the placeholder rows. */
.sk-wrap{display:block;width:100%}


/* ============================================================================
   9. AI STATES

   The AI companion, the health summary and the nudges all produce text after a
   wait that is genuinely variable — a local Ollama model, Claude over the network,
   or an instant rule-based fallback. A generic spinner says none of that. These
   states say "something is being worked out", which is what makes a wait feel
   deliberate instead of broken.

   Restraint on purpose: this is a clinical product, and a reply about someone's
   mental health should not arrive with a flourish. Everything here is quiet, and
   every piece of it stops under prefers-reduced-motion.
   ========================================================================== */

/* ---- Thinking. Three dots that breathe rather than bounce — bounce reads playful,
   which is wrong next to a crisis-support button. */
.ai-thinking{display:inline-flex;align-items:center;gap:5px;padding:12px 16px}
.ai-thinking i{
  width:6px;height:6px;border-radius:50%;background:var(--muted);
  animation:ai-breathe 1.4s var(--ease-in-out) infinite;
}
.ai-thinking i:nth-child(2){animation-delay:.18s}
.ai-thinking i:nth-child(3){animation-delay:.36s}
@keyframes ai-breathe{0%,100%{opacity:.25;transform:scale(.8)}45%{opacity:1;transform:scale(1)}}

/* ---- Streaming text. A cursor that holds while text arrives, so a pause in the
   stream reads as the model still working rather than as the answer being finished. */
.ai-streaming::after{
  content:"";display:inline-block;width:2px;height:1em;margin-left:2px;
  background:var(--accent);vertical-align:-.15em;
  animation:ai-caret 1s steps(1) infinite;
}
@keyframes ai-caret{0%,49%{opacity:1}50%,100%{opacity:0}}

/* ---- Arrival. Each line fades up as it lands instead of the block appearing at once,
   which is what makes a generated answer feel composed rather than pasted. */
.ai-line{animation:ai-arrive .34s var(--ease-out) both}
@keyframes ai-arrive{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}

/* ---- Working surface. While a result is being generated the panel gets a slow sheen
   along its accent edge — visible enough to say "still going", quiet enough to ignore. */
.ai-result.is-generating{position:relative;overflow:hidden}
.ai-result.is-generating::before{
  content:"";position:absolute;left:0;top:0;bottom:0;width:4px;
  background:linear-gradient(180deg,transparent,var(--accent),transparent);
  animation:ai-sweep 1.8s var(--ease-in-out) infinite;
}
@keyframes ai-sweep{0%{transform:translateY(-100%)}100%{transform:translateY(100%)}}

/* ---- Confidence. A generated suggestion is not a clinical judgement, and the interface
   should not present the two identically. */
.ai-tag{
  display:inline-flex;align-items:center;gap:5px;
  font-size:11px;font-weight:600;letter-spacing:.02em;
  color:var(--muted);background:var(--field);
  border:1px solid var(--line);border-radius:var(--r-pill);
  padding:3px 9px;
}
.ai-tag::before{content:"";width:5px;height:5px;border-radius:50%;background:var(--accent)}

/* Reduced motion: the states stay, the movement goes. Someone who cannot tolerate
   animation still needs to know the model is working. */
@media (prefers-reduced-motion:reduce){
  .ai-thinking i{animation:none;opacity:.55}
  .ai-streaming::after,.ai-result.is-generating::before{animation:none}
  .ai-line{animation:none}
}


/* ============================================================================
   10. DASHBOARD HIERARCHY

   The patient dashboard was sixteen sections of equal weight, ordered by the sequence
   in which features had shipped. "Upcoming appointments" — the reason someone opens the
   page — sat thirteenth, below a mood tracker and a category browser, while four
   different call-to-action buttons competed above the fold.

   Reordered by how fast a thing decays: the session ahead, then anything that expires,
   then the daily habit, then evergreen browsing. These styles carry that hierarchy. The
   sections themselves stayed; only their weight and order changed.
   ========================================================================== */

/* ---- NEXT: the one card the page exists for. Everything else on the page is a plain
   surface, so this earns its prominence from tint and scale rather than from more
   shadow — a heavier shadow would just start an arms race with the cards below it. */
.dash-next{
  display:flex;align-items:center;justify-content:space-between;gap:var(--s-5);
  flex-wrap:wrap;
  background:var(--accent-soft);
  border:1px solid var(--line);
  border-color:color-mix(in srgb,var(--accent) 18%,transparent);
  border-radius:var(--r-lg);
  padding:var(--s-5) var(--s-6);
  margin-bottom:var(--s-5);
  position:relative;overflow:hidden;
}
/* A rail rather than a full tint edge: reads as "this one" without turning the card into
   a banner, which is what the three stacked banners already were. */
.dash-next::before{
  content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--accent);
}
.dash-next-main{min-width:0;flex:1 1 260px}
.dash-next-eyebrow{
  display:block;font-size:11.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
  color:var(--accent);margin-bottom:var(--s-1);
}
.dash-next-title{font-size:19px;font-weight:700;color:var(--ink);margin:0;letter-spacing:-.01em}
.dash-next-meta{font-size:13.5px;color:var(--navy-2);margin:var(--s-1) 0 0}
.dash-next-actions{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap}
.dash-countdown{
  font-size:13.5px;font-weight:600;color:var(--navy-2);
  padding:var(--s-2) var(--s-3);
  background:var(--card);border-radius:var(--r-pill);
}

/* Live state. The only looping animation on the page, and only when a session is
   genuinely joinable — a pulse that runs all day teaches people to ignore it. */
.dash-next.is-live{
  background:var(--ok-soft);
  background:color-mix(in srgb,var(--ok) 10%,var(--card));
  border-color:var(--ok);
  border-color:color-mix(in srgb,var(--ok) 32%,transparent);
}
.dash-next.is-live::before{background:var(--ok)}
.dash-next.is-live .dash-next-eyebrow{color:var(--ok)}
@media (prefers-reduced-motion:no-preference){
  .dash-join{animation:dash-live 2.4s var(--ease-in-out) infinite}
}
@keyframes dash-live{
  0%,100%{box-shadow:0 0 0 0 var(--accent-soft);box-shadow:0 0 0 0 color-mix(in srgb,var(--accent) 40%,transparent)}
  50%{box-shadow:0 0 0 7px transparent}
}

/* Nothing booked. Same footprint, so the page does not reflow depending on whether the
   person has a session — and the space asks for the first booking instead of sitting
   empty, which is the most valuable thing an empty dashboard can do. */
.dash-next-empty{
  background:var(--card);
  border:1px dashed var(--line);
}
.dash-next-empty::before{display:none}
.dash-next-empty .dash-next-eyebrow{color:var(--muted)}

/* ---- ATTENTION: open loops, grouped. Three separate full-width banners each shouted at
   the same volume as the page; one list of rows reads as a short to-do and stops fighting
   the card above it. */
.dash-attention{
  background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);
  margin-bottom:var(--s-6);overflow:hidden;
}
.dash-todo{
  display:flex;align-items:center;gap:var(--s-3);
  padding:var(--s-4) var(--s-5);
  border-bottom:1px solid var(--line-2);
}
.dash-todo:last-child{border-bottom:0}
.dash-todo-ico{
  width:30px;height:30px;flex:0 0 auto;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  background:var(--gold-soft);color:var(--gold-text);font-size:13px;
}
.dash-todo-body{flex:1;min-width:0}
.dash-todo-body b{display:block;font-size:14px;color:var(--ink);font-weight:600}
.dash-todo-body small{display:block;color:var(--muted);font-size:12.5px;margin-top:2px}

/* ---- Section rhythm. Headings were only used in two places, so the page read as one
   undifferentiated scroll. Consistent spacing above each is what turns a stack into
   sections a person can skim past. */
.dash-section{margin:var(--s-10) 0 var(--s-4)}
.dash-section:first-of-type{margin-top:var(--s-8)}

/* ---- Quick actions and activity tiles became real <button>s rather than clickable
   <div>s. A div with onclick is invisible to keyboard and screen-reader users — it cannot
   be tabbed to and is not announced as a control — so these were unreachable for anyone
   not using a mouse. Resetting the button defaults keeps the existing look. */
.qa,.activity-tile{
  font:inherit;text-align:left;width:100%;
  border-width:1px;border-style:solid;
  -webkit-appearance:none;appearance:none;
}
.qa{border-color:var(--line);width:auto}
.activity-tile{border-color:transparent}

@media (max-width:640px){
  .dash-next{padding:var(--s-4);gap:var(--s-4)}
  .dash-next-actions{width:100%}
  .dash-next-actions .btn,.dash-join{flex:1;text-align:center}
  .dash-todo{padding:var(--s-3) var(--s-4);flex-wrap:wrap}
  .dash-todo .btn{margin-left:auto}
  .quick-actions .qa{flex:1 1 calc(50% - var(--s-3))}
}


/* ---- Daily check-in ---------------------------------------------------------------
   The dashboard used to carry two mood widgets; this is the merged one. Styles here cover
   the states the old pair never had between them: a logged state that still offers detail,
   and the optional slider panel that appears only if asked for. */
.qmb-head{margin-bottom:var(--s-4)}
.qmb-done-row{display:flex;align-items:center;justify-content:space-between;gap:var(--s-3);flex-wrap:wrap}
.qmb-more{font-size:12.5px;padding:var(--s-2) var(--s-3)}
.qmb-detail{
  margin-top:var(--s-4);padding-top:var(--s-4);
  border-top:1px solid var(--line-2);
  animation:hq-rise .3s var(--ease-out) both;
}
.qmb-trend{margin-top:var(--s-5);padding-top:var(--s-4);border-top:1px solid var(--line-2)}

/* A slider row has to stay usable one-handed on a phone, where the old three-column
   layout squeezed the track to about 90px. */
@media (max-width:560px){
  .mood-row{display:grid;grid-template-columns:1fr auto;gap:var(--s-2) var(--s-3);align-items:center}
  .mood-row input[type="range"]{grid-column:1/-1;width:100%}
}
@media (pointer:coarse){
  .mood-row input[type="range"]{height:32px}
}


/* ---- Session rows: past / now -----------------------------------------------------
   Every row in "Today's schedule" looked identical whether it had finished an hour ago or
   started in five minutes, so the list could only be read with a clock in hand. Finished
   sessions stay visible — a clinician checks who they have already seen — but recede. */
.srow.is-past{opacity:.5}
.srow.is-past .stag{background:var(--field);color:var(--muted)}

/* The one in progress. A left rail rather than a fill, so the row is marked without the
   list turning into a block of colour. */
.srow.is-now{
  position:relative;
  background:var(--ok-soft);
  background:color-mix(in srgb,var(--ok) 8%,transparent);
  margin-inline:calc(var(--s-4) * -1);
  padding-inline:var(--s-4);
  border-radius:var(--r-sm);
}
.srow.is-now::before{
  content:"";position:absolute;left:0;top:6px;bottom:6px;width:3px;
  border-radius:var(--r-pill);background:var(--ok);
}
.srow.is-now .time{color:var(--ok)}

/* .join became an <a> so it can actually navigate — it was a <button> with no handler, so
   clicking Join on your own session did nothing at all. */
a.join{display:inline-flex;align-items:center;justify-content:center;text-decoration:none}
@media (pointer:coarse){ a.join,.srow .join{min-height:44px} }


/* ---- Admin action queue ------------------------------------------------------------
   The admin dashboard led with five totals and four trend charts and never said what was
   waiting — "Pending doctors" was a button with no number on it, so nothing pending and
   forty pending looked the same. These cards are the work; the totals below them are the
   monthly review. */
.admin-queue{margin-bottom:var(--s-6)}
.admin-queue-head{
  display:flex;align-items:baseline;justify-content:space-between;
  gap:var(--s-3);margin-bottom:var(--s-4);flex-wrap:wrap;
}
.admin-queue-grid{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:var(--s-3);
}
.admin-queue-item{
  display:flex;flex-direction:column;gap:2px;
  background:var(--card);
  border:1px solid var(--line);
  border-left:3px solid var(--accent);
  border-radius:var(--r-md);
  padding:var(--s-4) var(--s-5);
  text-decoration:none;
  transition:box-shadow var(--t-base) var(--ease-out),border-color var(--t-base) var(--ease-out);
}
@media (hover:hover){
  .admin-queue-item:hover{box-shadow:var(--e-2);border-color:var(--accent)}
}
.admin-queue-item .n{font-size:26px;font-weight:800;color:var(--ink);line-height:1.1}
.admin-queue-item .l{font-size:13px;font-weight:600;color:var(--ink)}
/* The reason it matters, not just the count. A number with no consequence attached gets
   triaged by whichever is largest rather than by what actually costs something. */
.admin-queue-item .why{font-size:11.5px;color:var(--muted);margin-top:var(--s-1)}

/* Erasures carry a statutory deadline, so they are the one queue allowed to look different
   from the others. Not red-alert styling — nothing here is an emergency — just first and
   unmistakably its own thing. */
.admin-queue-item.is-urgent{border-left-color:var(--maroon)}
.admin-queue-item.is-urgent .n{color:var(--maroon)}
@media (hover:hover){
  .admin-queue-item.is-urgent:hover{border-color:var(--maroon)}
}

/* An explicitly empty queue. If the section simply disappeared, a clear day would be
   indistinguishable from a feature that had not loaded. */
.admin-queue.is-clear{
  background:var(--ok-soft);border-radius:var(--r-md);
  padding:var(--s-4) var(--s-5);
}
.admin-queue-clear{color:var(--ok);font-weight:600;font-size:14px}

.admin-inline-n{font-size:20px}
.admin-role-row{padding-block:var(--s-2)}


/* ---- HR: department wellbeing basis ------------------------------------------------
   These scores are employees' mental-health data shown to their employer, and the number
   was previously presented bare. A confident-looking 68 built from two check-ins reads
   identically to one built from two hundred, which is the failure mode that matters most
   on this page. The basis line sits directly under each bar, and rows drawn from thin data
   are dimmed rather than hidden — withholding them entirely would leave HR wondering
   whether a department was missing or simply quiet. */
.dept-row.is-thin{opacity:.62}
.dept-basis{
  font-size:11.5px;color:var(--muted);
  margin:calc(var(--s-1) * -1) 0 var(--s-4);
  padding-left:2px;
}
.dept-row + .dept-basis{margin-top:2px}

/* ---- Two classes the HR dashboard has always used and that were never defined anywhere.
   .list-row rendered as an unstyled stack of divs and .stat-row left its two stats
   unaligned — pre-existing, and easy to miss because nothing errors when a class simply
   does not exist. */
.list-row{
  display:flex;align-items:center;justify-content:space-between;gap:var(--s-3);
  padding:var(--s-3) 0;border-bottom:1px solid var(--line-2);
}
.list-row:last-of-type{border-bottom:0}
.list-row .nm b{display:block;font-size:14px;color:var(--ink)}
.list-row .nm small{display:block;color:var(--muted);font-size:12.5px;margin-top:2px}
.stat-row{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--s-3)}


/* ============================================================================
   11. ADMIN LIST PAGES

   Fifteen tables across the admin and HR sections all use .report-table, so this is
   the one place that lifts every list at once.
   ========================================================================== */

/* Headers stay put on long lists. An admin scrolling 200 users lost the column names
   after the first screen and had to scroll back up to remember which column was which. */
.report-table thead th,.simple-table thead th{
  position:sticky;top:0;z-index:2;
  background:var(--field);
}

/* Numeric columns right-align and use tabular figures, so amounts and counts line up on
   the decimal instead of ragging. Opt-in per column via <th data-numeric>. */
.report-table th[data-numeric],.report-table td[data-numeric]{
  text-align:right;font-variant-numeric:tabular-nums;
}

/* Row separation without hover. The zebra was hover-only, which meant a static screenshot
   of an admin table — or anyone using touch — got an undifferentiated grid. */
.report-table tbody tr:nth-child(even) td,
.simple-table tbody tr:nth-child(even) td{background:var(--line-2)}
@media (hover:hover){
  .report-table tbody tr:hover td{background:var(--accent-soft)}
}

/* Actions column: keep buttons on one line and hard against the right edge, where a
   list's actions belong. */
.report-table td.actions{white-space:nowrap;text-align:right}
.report-table td.actions .btn{padding:5px 10px;font-size:12px}

/* Long free-text cells (emails, notes, addresses) were the reason tables sprawled — one
   long value stretched its column and pushed everything else off screen. */
.report-table td.truncate{max-width:22ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* On narrow screens the header cells are the ones that must not wrap; body cells still may,
   which keeps the scroll distance proportional to the data rather than to the longest
   sentence in it. */
@media (max-width:900px){
  .report-table th{white-space:nowrap}
}

/* ---- "No results" is not "nothing here" ------------------------------------------
   Both rendered as the same empty table. They mean opposite things: one says your filter
   is too narrow, the other says the feature has no data yet — and the fix for each is the
   opposite of the fix for the other. */
.list-status{
  text-align:center;padding:var(--s-10) var(--s-5);color:var(--muted);
}
.list-status b{display:block;color:var(--ink);font-size:15px;margin-bottom:var(--s-1)}
.list-status p{margin:0 auto;max-width:40ch;font-size:13.5px;line-height:1.6}
.list-status .btn{margin-top:var(--s-4)}

/* ---- Result header ---------------------------------------------------------------- */
.list-head{
  display:flex;align-items:baseline;justify-content:space-between;
  gap:var(--s-3);flex-wrap:wrap;margin-bottom:var(--s-3);
}
.list-head .count{font-size:13px;color:var(--muted);font-variant-numeric:tabular-nums}


/* ---- Toast variants ---------------------------------------------------------------
   One surface now carries both server-pushed notifications and the confirmation a page
   raises after an action. Colour distinguishes them, but never alone — the dot changes
   shape too, because "did my save work?" answered only in green is not answered for
   everyone. */
.toast-success{background:var(--ok);color:#fff}
.toast-success .dotlive{background:#fff;border-radius:0;width:9px;height:9px;
  clip-path:polygon(14% 44%,0 58%,40% 100%,100% 16%,86% 2%,38% 70%)}
.toast-error{background:var(--maroon);color:#fff}
.toast-error .dotlive{background:#fff;border-radius:0;width:9px;height:9px;
  clip-path:polygon(20% 0,0 20%,30% 50%,0 80%,20% 100%,50% 70%,80% 100%,100% 80%,70% 50%,100% 20%,80% 0,50% 30%)}
.toast-info{background:var(--navy)}

/* On a phone the toast belongs at the top edge, full width — the desktop corner position
   puts it under the thumb and over the content it is describing. */
@media (max-width:640px){
  .live-toasts{top:0;left:0;right:0;max-width:none;padding:var(--s-2)}
  .toast{border-radius:var(--r-md)}
}
@supports (padding:max(0px)){
  @media (max-width:640px){ .live-toasts{padding-top:max(var(--s-2),env(safe-area-inset-top))} }
}


/* ---- Tooltips on a phone ------------------------------------------------------------
   The bubble is centred on its trigger, which runs off the screen when the trigger sits
   near an edge — and on a 360px viewport most things are near an edge. Constrained to the
   viewport rather than to the trigger. */
@media (max-width:640px){
  .hq-tip::after{max-width:min(230px,calc(100vw - 32px))}
}
/* A tapped tooltip needs a way to close. Focus does that on its own — tapping anywhere else
   blurs the element — but the trigger must be visibly tappable first, or nobody discovers
   that the little ⓘ does anything at all. */
@media (pointer:coarse){
  .hq-tip{min-height:32px;display:inline-flex;align-items:center}
  .hq-tip-icon{width:20px;height:20px;font-size:12px}
}


/* ============================================================================
   12. iOS / WebKit

   Android and iOS fail differently. These are the ones specific to WebKit, and the
   one that matters most is not here at all — it is `viewport-fit=cover` in the
   viewport meta tag, without which every env(safe-area-inset-*) rule above silently
   resolves to zero on every iPhone.
   ========================================================================== */

/* ---- Rubber-band scrolling: deliberately NOT "fixed".
   overscroll-behavior is honoured by Chrome and Firefox but ignored by iOS Safari, so the
   scroll-chaining rule earlier does nothing there. The usual workaround is to stop the
   document scrolling and let an inner container own it — html,body{overflow:hidden} plus
   .hq-main{overflow-y:auto}.

   That was written here and then removed, because the fifteen pages on LoginLayout — login,
   every signup, guest call join, the public blog — have no .hq-main at all. On those, the
   body would stop scrolling and nothing would take over: a signup form that simply cannot be
   scrolled on an iPhone. Trading a cosmetic bounce for an unusable signup page is not a
   trade worth making, and doing it properly needs a JS scroll lock that knows what is open.

   The bounce itself is native iOS behaviour that users expect, so it stays. */

/* ---- Long-press. Holding a link or button on iOS pops the share/preview sheet, which on a
   control is never what was wanted. Content keeps it — people long-press a phone number to
   call it. */
a,button,[role="button"],.chip,.slotchip,.qa,.activity-tile{-webkit-touch-callout:none}
p,td,.kv .val,[data-selectable]{-webkit-touch-callout:default}

/* ---- Form controls. iOS applies its own rounded, shaded chrome to text inputs and ignores
   their background-color until appearance is reset — which is why a themed field can look
   right everywhere and grey on an iPhone.

   Scoped to text-like inputs on purpose. A blanket `input { appearance: none }` would strip
   checkboxes and radios down to invisible empty boxes and delete the track and thumb from
   every range slider — including the mood sliders, which would become an unusable blank
   strip. The type list is explicit for that reason. */
.field,
input[type="text"],input[type="email"],input[type="tel"],input[type="number"],
input[type="password"],input[type="search"],input[type="url"],
textarea{
  -webkit-appearance:none;appearance:none;
}
/* Checkboxes, radios, ranges, dates and file pickers keep their native rendering — on iOS
   the native control is both better and the one people recognise. */
input[type="checkbox"],input[type="radio"],input[type="range"],
input[type="date"],input[type="time"],input[type="datetime-local"],input[type="file"]{
  -webkit-appearance:auto;appearance:auto;
}
/* iOS collapses an empty date field to zero height inside a flex row. */
input[type="date"],input[type="time"],input[type="datetime-local"]{min-height:1.4em}

/* ---- Autofill. Safari paints a hard yellow over autofilled fields, ignoring the theme —
   glaring in dark mode and unreadable when the text colour is light. The inset shadow trick
   is the only way to override it. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
select:-webkit-autofill{
  -webkit-box-shadow:0 0 0 1000px var(--field) inset;
  -webkit-text-fill-color:var(--ink);
  caret-color:var(--ink);
  transition:background-color 9999s ease-in-out 0s;
}

/* ---- position: sticky needs the old prefix on iOS versions before 13. Harmless everywhere
   else, since the unprefixed declaration follows and wins. */
.topbar,.report-table thead th,.simple-table thead th,.xd-bar{
  position:-webkit-sticky;position:sticky;
}

/* ---- The on-screen keyboard. iOS shrinks the visual viewport but leaves fixed elements
   positioned against the layout viewport, so a fixed footer or FAB ends up underneath the
   keyboard. dvh follows the visual viewport and keeps them reachable. */
@supports (height:100dvh){
  .modal-overlay{max-height:100dvh}
  .fab-panel{max-height:min(70dvh,600px)}
}

/* ---- Momentum scrolling for every scrollable region, not just the few listed earlier. */
[class*="-scroll"],.modal-card,.chat-log,.fab-log,.msg-contacts{-webkit-overflow-scrolling:touch}

/* ---- Text size. iOS enlarges text in landscape unless told not to; the fluid scale in
   section 1 already handles sizing, and this stops Safari overriding it. */
body{-webkit-text-size-adjust:100%;text-size-adjust:100%}


/* ---- Chat attachments --------------------------------------------------------------
   A client sending a lab report to their therapist, or an expert sending back a care plan.
   The chip shows what the file is before it is opened, which matters here: the person may
   be on a train, or sharing their screen in a session. */
.chat-attach{
  display:flex;align-items:center;gap:var(--s-2);
  background:var(--card);border:1px solid var(--line);border-radius:var(--r-md);
  padding:var(--s-2) var(--s-3);
  max-width:100%;
}
.chat-row.me .chat-attach{background:rgba(255,255,255,.14);border-color:rgba(255,255,255,.28)}
.chat-attach-ico{font-size:17px;flex:0 0 auto}
.chat-attach-meta{min-width:0;flex:1;display:flex;flex-direction:column;line-height:1.3}
.chat-attach-meta b{font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.chat-attach-meta small{font-size:11px;opacity:.75;font-variant-numeric:tabular-nums}
.chat-attach .btn{padding:5px 10px;font-size:12px;flex:0 0 auto}
.chat-attach-img{
  display:block;max-width:100%;max-height:340px;
  border-radius:var(--r-md);object-fit:contain;background:var(--field);
}

/* Selected-but-not-yet-sent, above the input. Shown before sending so a wrong file can be
   removed rather than recalled. */
.chat-pending{
  display:flex;align-items:center;gap:var(--s-2);
  background:var(--accent-soft);border-radius:var(--r-md);
  padding:var(--s-2) var(--s-3);margin-bottom:var(--s-2);
}

/* The attach control. The real <input type="file"> stays in the DOM and focusable — hiding
   it with display:none would take it out of the tab order and make attaching impossible
   without a mouse — so it is made invisible but still reachable, with the label as its
   visible surface. */
.chat-attach-btn{
  position:relative;overflow:hidden;
  display:inline-flex;align-items:center;justify-content:center;
  flex:0 0 auto;width:42px;height:42px;
  border:1px solid var(--line);border-radius:var(--r-md);
  background:var(--card);cursor:pointer;font-size:17px;
}
.chat-attach-btn input[type="file"]{
  position:absolute;inset:0;opacity:0;cursor:pointer;
  width:100%;height:100%;
}
.chat-attach-btn:focus-within{box-shadow:var(--focus-ring)}
@media (hover:hover){ .chat-attach-btn:hover{border-color:var(--accent)} }
@media (pointer:coarse){ .chat-attach-btn{width:44px;height:44px} }


/* ---- Call side panel ---------------------------------------------------------------
   Chat, transcript and (for the expert) notes share one column beside the video. Tabs
   rather than three stacked panes, because on the width left over next to a video feed
   three panes each get too little room to be usable. */
.call-side{display:flex;flex-direction:column;min-height:0}
.call-tabs{display:flex;gap:2px;border-bottom:1px solid var(--line);margin-bottom:var(--s-3)}
.call-tab{
  flex:1;padding:var(--s-2) var(--s-3);
  background:none;border:none;border-bottom:2px solid transparent;
  font:inherit;font-size:13px;font-weight:600;color:var(--muted);cursor:pointer;
  display:inline-flex;align-items:center;justify-content:center;gap:6px;
}
.call-tab.active{color:var(--accent);border-bottom-color:var(--accent)}
.call-tab-dot{
  min-width:17px;height:17px;padding:0 4px;border-radius:var(--r-pill);
  background:var(--accent);color:var(--on-accent);
  font-size:10.5px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;
}
.call-panel{flex:1;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch}
.call-notes{display:flex;flex-direction:column}
.call-notes-box{flex:1;min-height:180px;resize:vertical;line-height:1.6}

/* On a phone the video takes the width and the panel sits under it — side by side would
   leave both too narrow to use. */
@media (max-width:900px){
  .call-side{max-height:52vh}
  .call-tab{font-size:12.5px}
}


/* ============================================================================
   13. BRAND THEME BEHAVIOUR

   The tokens live in app.css; these are the rules that use the ones the old palette
   had no equivalent for — links and the WhatsApp button — plus the type assignment.
   ========================================================================== */

/* ---- Type. Marcellus carries the headings, Montserrat everything else. Applied here
   rather than scattered, so changing the pairing is one edit. */
body{font-family:var(--sans)}
h1,h2,h3,h4,h5,h6,
.hello,.section-h,.dash-next-title,.qmb-title{
  font-family:var(--display);
  font-weight:400;              /* Marcellus ships one weight; asking for 700 makes the
                                   browser synthesise a fake bold that looks smeared */
  letter-spacing:-.005em;
}
/* Marcellus has a taller x-height than the sans it replaces, so headings read larger at
   the same px. Pulled back slightly rather than leaving every heading a size too big. */
h1,.hello{font-size:clamp(22px,1.4rem + .4vw,26px)}

/* ---- Links. The one place the brand orange had to move: #E8673A is 3.03:1 on the cream
   background, and link text needs 4.5. Same hue, darker, and still obviously the brand. */
a:not(.btn):not(.cta):not(.qa):not(.activity-tile):not(.chip):not(.nav a){color:var(--link)}
@media (hover:hover){
  a:not(.btn):not(.cta):not(.qa):not(.activity-tile):not(.chip):hover{color:var(--link-hover)}
}
/* Colour is never the only signal for a link inside a paragraph — someone who cannot
   distinguish orange from the body text around it has nothing else to go on. */
p a,li a,.hint-text a,td a{text-decoration:underline;text-underline-offset:2px}

/* ---- WhatsApp. Their green is fixed by their brand; white on it is 1.98:1, which is why
   the label here is dark. */
.btn-whatsapp,.whatsapp-btn{
  background:var(--whatsapp);
  color:#1A1A1A;
  border:none;
  font-weight:600;
}
@media (hover:hover){ .btn-whatsapp:hover,.whatsapp-btn:hover{filter:brightness(1.06)} }

/* ---- Hover brightens rather than darkens, because the label is dark. Darkening would
   move contrast the wrong way; this lifts 5.33 to 6.58 and still reads as a response. */
@media (hover:hover){
  .btn.primary:hover:not(:disabled),.btn.rose:hover:not(:disabled),.cta:hover:not(:disabled){
    background:var(--rose-deep);
  }
}

/* ---- Cards sit on cream, so their own tint needs a boundary or the two warm surfaces
   blur into each other. */
.card,.stat,.exp-card,.pkg-card{border:1px solid var(--line)}


/* ---- Face scan notices -------------------------------------------------------------
   Three states that used to share one style, which meant a camera failure and a reading
   worth mentioning to a doctor looked identical. */
.fs-notice-err{
  border-left:3px solid var(--err);
  background:var(--maroon-soft);
}
.fs-notice-err b{color:var(--err)}

/* A vitals figure outside the usual resting range. Deliberately not red: it is a prompt to
   mention something at a consultation, not an alarm, and styling it as an emergency on a
   mental-health platform would frighten people over a camera estimate. */
.fs-notice-attention{
  border-left:3px solid var(--accent);
  background:var(--accent-soft);
  text-align:left;
}
.fs-notice-attention b{color:var(--ink)}
.fs-notice-attention small{display:block;line-height:1.6;margin-top:4px}
.fs-notice-attention .btn{display:inline-flex}

/* .fs-pop was on every result card and defined nowhere. It was also unnecessary:
   .fs-result-card in app.css already runs fs-pop-in, and the inline animation-delay was
   staggering that. Defining it here would have redeclared the keyframe and quietly changed
   the existing animation, so the class was removed from the markup instead. */
@media (prefers-reduced-motion:reduce){ .fs-result-card{animation:none;opacity:1} }


/* ============================================================================
   14. SECOND-PASS UTILITIES

   Patterns that appeared five or more times as inline styles. Naming them is not
   tidiness for its own sake: an inline style cannot be changed in one place, so
   twenty-five subtitles meant twenty-five separate decisions that had already
   drifted into three different spacings.
   ========================================================================== */

/* The line under a section heading. One spacing, everywhere. */
.section-sub{
  color:var(--muted);
  font-size:13px;
  line-height:1.6;
  margin:var(--s-1) 0 var(--s-4);
}

/* Right-aligned numeric table columns. Tabular figures so digits line up column-wise —
   the reason to right-align in the first place. */
.num-col-sm,.num-col,.num-col-lg{text-align:right;font-variant-numeric:tabular-nums}
.num-col-sm{width:70px}
.num-col{width:80px}
.num-col-lg{width:90px}

/* Filter-row fields. The flex ratio is the layout; min-width stops a select collapsing
   below its own longest option. */
.filter-wide{flex:2;min-width:200px}
.filter-mid{flex:1;min-width:170px}

.sec-underline{border-bottom:2px solid var(--gold-soft);padding-bottom:var(--s-2)}
.head-sp{margin:var(--s-5) 0 var(--s-2)}

/* A section that is a link target. scroll-margin keeps the heading clear of the sticky
   topbar when jumped to — without it the anchor lands underneath it. */
/* --s-9 does not exist — the scale skips from 8 to 10. The original 36px sits
   between them; snapped up, which is what the scale is for. */
.anchor-block{margin-bottom:var(--s-10);scroll-margin-top:var(--s-5)}

.cap-lg{max-width:560px}
.cap-md{max-width:150px}
.row-pad{padding:var(--s-2) 0}

/* ---- .fs-result-burst -----------------------------------------------------------------
   Restored. These rules were deleted along with the celebration on the face-scan result,
   where a confetti burst above a resting pulse of 120 was the wrong response — but two other
   pages use the same class for things that ARE achievements: finishing a certification and
   completing a study-success assessment. Only the face-scan usage was wrong; the class was
   not. Checking one caller before deleting a shared rule was the mistake. */
.fs-result-burst{
  font-size:38px;
  position:relative;
  display:inline-block;
  animation:fs-pop-in .5s cubic-bezier(.34,1.56,.64,1);
}
.fs-result-burst::before,.fs-result-burst::after{
  content:"";position:absolute;inset:-14px;border-radius:50%;pointer-events:none;
}
.fs-result-burst::before{
  background:radial-gradient(circle,var(--gold) 0 3px,transparent 4px) 0 0/22px 22px;
  opacity:.5;
}
@media (prefers-reduced-motion:reduce){
  .fs-result-burst{animation:none}
  .fs-result-burst::before,.fs-result-burst::after{display:none}
}


/* ---- Confirmation dialog ------------------------------------------------------------
   Used before anything is deleted. Deliberately small — a full-width sheet reads as a page
   and invites scanning past it; a compact card reads as an interruption, which is the point. */
.confirm-card{max-width:420px;text-align:center}
.confirm-icon{
  width:46px;height:46px;margin:0 auto var(--s-3);
  display:flex;align-items:center;justify-content:center;
  border-radius:50%;font-size:20px;
  background:var(--maroon-soft);color:var(--maroon);
}
.confirm-actions{justify-content:center;margin-top:var(--s-5)}

/* The destructive button is red-filled, which nothing else in the app is — so it cannot be
   mistaken for the primary action it sits next to. */
.btn-danger{
  background:var(--maroon);
  color:#fff;
  border:1px solid var(--maroon);
}
@media (hover:hover){ .btn-danger:hover:not(:disabled){filter:brightness(1.1)} }
.btn-danger:disabled{opacity:.6}

/* ---- Third-pass utilities ------------------------------------------------------------ */
.break-all{word-break:break-all}
.nowrap{white-space:nowrap}
.cap-xl{max-width:640px}
.cap-lg-sm{max-width:200px}
.fixed-200{flex:0 0 200px}
.fixed-110{flex:0 0 110px}

/* ---- Blog sidebar: the four-step journey block ------------------------------------- */
.hq-steps{list-style:none;counter-reset:hqstep;margin:0;padding:0}
.hq-steps li{
  counter-increment:hqstep;position:relative;padding:0 0 var(--s-3) var(--s-8);
  font-size:12.5px;line-height:1.5;
}
.hq-steps li::before{
  content:counter(hqstep);position:absolute;left:0;top:0;
  width:22px;height:22px;border-radius:50%;
  background:var(--accent-soft);color:var(--gold-text);
  font-weight:700;font-size:11.5px;display:flex;align-items:center;justify-content:center;
}
/* The vertical thread between steps — it is what makes four cards read as one sequence. */
.hq-steps li:not(:last-child)::after{
  content:"";position:absolute;left:10px;top:24px;bottom:4px;width:1px;background:var(--line);
}
.hq-steps li b{display:block;color:var(--ink);font-size:13px}
.hq-steps li span{color:var(--muted)}
